improvement: some new gadgets added to dashboard. some new functionality added to existed gadgets.

This commit is contained in:
2026-07-14 03:06:56 +03:30
parent ec2b23f4b1
commit d383a4abc3
34 changed files with 944 additions and 191 deletions

View File

@@ -8,6 +8,7 @@ import { useAuth } from '@/lib/hooks/useAuth';
import { useToast } from '@/lib/hooks/useToast';
import { canEditCases, canEditTasks } from '@/components/shared/permissions';
import { CaseDetailPanel, CaseTaskProgressBar } from '@/components/ui/lab/CaseDetailPanel';
import { LabCaseProsthesisGroupsList } from '@/components/ui/lab/LabCaseProsthesisGroupsList';
import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel';
import {
formatCaseDateTime,
@@ -18,6 +19,7 @@ import { notificationsApi } from '@/lib/api/notifications';
import { notifyTabBadgesChanged } from '@/lib/tabBadgeUtils';
import { casesApi } from '@/lib/api/cases';
import { tasksApi } from '@/lib/api/tasks';
import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog';
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
import { treatmentTypeLabelFromCatalog } from '@/components/shared/treatmentTypeDisplay';
import { Badge } from '@/components/ui/shared/Badge';
@@ -26,7 +28,7 @@ import { MobileDetailBackButton } from '@/components/ui/shared/MobileDetailBackB
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
import { SearchBar } from '@/components/ui/shared/SearchBar';
import { AppDateInput } from '@/components/ui/shared/AppDateInput';
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
import type { ProsthesisCatalogEntry, TreatmentCatalogEntry } from '@/types/treatment-catalog';
import type {
AssignableTaskStaff,
CasesFilterOptions,
@@ -48,7 +50,7 @@ export function CasesPage() {
const [search, setSearch] = useState('');
const [clinicId, setClinicId] = useState('');
const [treatmentType, setTreatmentType] = useState('');
const [prosthesisTypeCode, setProsthesisTypeCode] = useState('');
const [sentFrom, setSentFrom] = useState('');
const [sentTo, setSentTo] = useState('');
const [page, setPage] = useState(1);
@@ -62,8 +64,9 @@ export function CasesPage() {
});
const [filterOptions, setFilterOptions] = useState<CasesFilterOptions>({
clinics: [],
treatmentTypes: [],
prosthesisTypes: [],
});
const [prosthesisCatalog, setProsthesisCatalog] = useState<ProsthesisCatalogEntry[]>([]);
const [treatmentCatalog, setTreatmentCatalog] = useState<TreatmentCatalogEntry[]>([]);
const [selectedCaseId, setSelectedCaseId] = useState<string | null>(null);
@@ -80,7 +83,13 @@ export function CasesPage() {
const canEditComments = canEditTasks(currentOrganization);
const locale = user?.language ?? 'en';
const treatmentLabel = useCallback(
const prosthesisLabel = useCallback(
(code: string) =>
prosthesisCatalog.find((entry) => entry.code === code)?.label ?? code,
[prosthesisCatalog],
);
const treatmentDetailLabel = useCallback(
(type: string) => treatmentTypeLabelFromCatalog(type, treatmentCatalog),
[treatmentCatalog],
);
@@ -94,13 +103,13 @@ export function CasesPage() {
);
const hasActiveFilters = Boolean(
search.trim() || clinicId || treatmentType || sentFrom || sentTo,
search.trim() || clinicId || prosthesisTypeCode || sentFrom || sentTo,
);
const loadCases = async (params: {
q: string;
clinicOrganizationId: string;
treatmentType: string;
prosthesisTypeCode: string;
sentFrom: string;
sentTo: string;
page: number;
@@ -111,7 +120,7 @@ export function CasesPage() {
const response = await casesApi.list({
q: params.q.trim() || undefined,
clinicOrganizationId: params.clinicOrganizationId || undefined,
treatmentType: params.treatmentType || undefined,
prosthesisTypeCode: params.prosthesisTypeCode || undefined,
sentFrom: params.sentFrom || undefined,
sentTo: params.sentTo || undefined,
page: params.page,
@@ -148,6 +157,7 @@ export function CasesPage() {
useEffect(() => {
void casesApi.listFilterOptions().then((r) => setFilterOptions(r.data)).catch(() => {});
void prosthesisCatalogApi.list().then((r) => setProsthesisCatalog(r.data)).catch(() => {});
void treatmentCatalogApi.list().then((r) => setTreatmentCatalog(r.data)).catch(() => {});
if (canEdit) {
void casesApi.listAssignableStaff().then((r) => setAssignableStaff(r.data)).catch(() => {});
@@ -156,11 +166,6 @@ export function CasesPage() {
}, [canEdit]);
useEffect(() => {
const caseIdFromUrl = searchParams.get('caseId');
if (caseIdFromUrl) {
setSelectedCaseId(caseIdFromUrl);
setMobileDetailOpen(true);
}
const clinicFromUrl = searchParams.get('clinicOrganizationId');
if (clinicFromUrl) {
setClinicId(clinicFromUrl);
@@ -168,17 +173,37 @@ export function CasesPage() {
}, [searchParams]);
useEffect(() => {
if (!selectedCaseId) {
setMobileDetailOpen(false);
if (loadingList) return;
if (cases.length === 0) {
if (selectedCaseId !== null) {
setSelectedCaseId(null);
}
return;
}
}, [selectedCaseId]);
const urlCaseId = searchParams.get('caseId');
if (urlCaseId && cases.some((item) => item.id === urlCaseId)) {
if (selectedCaseId !== urlCaseId) {
setSelectedCaseId(urlCaseId);
setMobileDetailOpen(true);
}
return;
}
if (selectedCaseId && cases.some((item) => item.id === selectedCaseId)) {
return;
}
setSelectedCaseId(cases[0].id);
}, [cases, loadingList, searchParams, selectedCaseId]);
useEffect(() => {
const timeout = setTimeout(() => {
void loadCases({
q: search,
clinicOrganizationId: clinicId,
treatmentType,
prosthesisTypeCode,
sentFrom,
sentTo,
page,
@@ -186,7 +211,13 @@ export function CasesPage() {
}, search ? 300 : 0);
return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps -- debounced search + filter reload
}, [search, clinicId, treatmentType, sentFrom, sentTo, page]);
}, [search, clinicId, prosthesisTypeCode, sentFrom, sentTo, page]);
useEffect(() => {
if (!selectedCaseId) {
setMobileDetailOpen(false);
}
}, [selectedCaseId]);
useEffect(() => {
if (selectedCaseId) {
@@ -222,7 +253,7 @@ export function CasesPage() {
function clearFilters() {
setSearch('');
setClinicId('');
setTreatmentType('');
setProsthesisTypeCode('');
setSentFrom('');
setSentTo('');
setPage(1);
@@ -314,19 +345,19 @@ export function CasesPage() {
</label>
<label className="space-y-1">
<span className="text-xs font-medium text-text-muted">{t('filterTreatmentType')}</span>
<span className="text-xs font-medium text-text-muted">{t('filterProsthesisType')}</span>
<select
value={treatmentType}
value={prosthesisTypeCode}
onChange={(e) => {
setTreatmentType(e.target.value);
setProsthesisTypeCode(e.target.value);
setPage(1);
}}
className={filterSelectClass}
>
<option value="">{t('filterTreatmentTypeAll')}</option>
{filterOptions.treatmentTypes.map((type) => (
<option value="">{t('filterProsthesisTypeAll')}</option>
{filterOptions.prosthesisTypes.map((type) => (
<option key={type.code} value={type.code}>
{treatmentLabel(type.code)}
{prosthesisLabel(type.code)}
</option>
))}
</select>
@@ -410,17 +441,17 @@ export function CasesPage() {
/>
) : null}
</div>
<div className="text-xs text-text-muted mt-0.5">
{item.patient.mobile}
</div>
<div className="text-xs text-text-muted mt-0.5">{item.clinic.name}</div>
<div className="text-xs text-text-muted mt-1">
<div className="mt-1">
<LabCaseProsthesisGroupsList
groups={item.prosthesisGroups}
prosthesisCatalog={prosthesisCatalog}
/>
</div>
<div className="text-[10px] text-text-muted mt-1">
{formatCaseDateTime(item.sentAt, locale)}
</div>
<div className="text-xs text-text-muted mt-1 truncate">
{item.treatmentType ? treatmentLabel(item.treatmentType) : '—'}
</div>
<div className="mt-2">
<div className="mt-1.5">
<CaseTaskProgressBar
completed={item.taskProgress.completed}
total={item.taskProgress.total}
@@ -471,15 +502,15 @@ export function CasesPage() {
{mobileDetailOpen && selectedCaseId ? (
<MobileDetailBackButton onClick={() => setMobileDetailOpen(false)} />
) : null}
{!selectedCaseId ? (
<p className="text-sm text-text-muted">{t('selectCaseHint')}</p>
{!selectedCaseId && !loadingList && cases.length === 0 ? (
<p className="text-sm text-text-muted">{t('emptyList')}</p>
) : loadingDetail || !selectedCase ? (
<p className="text-sm text-text-muted">{tCommon('loading')}</p>
) : (
<CaseDetailPanel
labCase={selectedCase}
locale={locale}
treatmentLabel={treatmentLabel}
treatmentLabel={treatmentDetailLabel}
statusOptions={statusOptions}
loadAttachmentBlob={loadCaseAttachmentBlob}
showCommentsButton