'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { ToastStack } from '@/components/ui/shared/Toast'; import { formatApiErrorMessage } from '@/components/shared/formatApiError'; import { useAuth } from '@/lib/hooks/useAuth'; import { useToast } from '@/lib/hooks/useToast'; import { canEditCases } from '@/components/shared/permissions'; import { Badge, type BadgeVariant } from '@/components/ui/shared/Badge'; import { casesApi } from '@/lib/api/cases'; import { treatmentCatalogApi } from '@/lib/api/treatment-catalog'; import { treatmentTypeLabelFromCatalog } from '@/components/ui/treatment/treatmentTypeDisplay'; import { Button } from '@/components/ui/shared/Button'; import { FORM_SELECT_CLASS } from '@/components/ui/shared/formSelectStyles'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import type { TreatmentCatalogEntry } from '@/types/treatment-catalog'; import type { AssignableMember, CasesFilterOptions, LabCaseDetail, LabCaseListItem, LabTaskStatus, PaginatedLabCases, } from '@/types/cases'; const PAGE_SIZE = 20; const PRIORITY_OPTIONS = [1, 2, 3, 4, 5] as const; function taskStatusVariant(status: LabTaskStatus): BadgeVariant { switch (status) { case 'COMPLETED': return 'success'; case 'IN_PROGRESS': return 'default'; default: return 'warning'; } } function formatPatientName(patient: { firstName: string; lastName: string }) { return `${patient.firstName} ${patient.lastName}`.trim(); } function formatDateTime(value: string | null, locale: string) { if (!value) return '—'; return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short', }).format(new Date(value)); } function TaskProgressBar({ completed, total }: { completed: number; total: number }) { const pct = total > 0 ? Math.round((completed / total) * 100) : 0; return (
{completed}/{total} {pct}%
); } export default function CasesPage() { const t = useTranslations('cases'); const tTreatment = useTranslations('treatment'); const tCommon = useTranslations('common'); const { currentOrganization, user } = useAuth(); const toast = useToast(); const searchParams = useSearchParams(); const [search, setSearch] = useState(''); const [clinicId, setClinicId] = useState(''); const [treatmentType, setTreatmentType] = useState(''); const [sentFrom, setSentFrom] = useState(''); const [sentTo, setSentTo] = useState(''); const [page, setPage] = useState(1); const [cases, setCases] = useState([]); const [pagination, setPagination] = useState({ page: 1, limit: PAGE_SIZE, total: 0, totalPages: 1, }); const [filterOptions, setFilterOptions] = useState({ clinics: [], treatmentTypes: [], }); const [treatmentCatalog, setTreatmentCatalog] = useState([]); const [selectedCaseId, setSelectedCaseId] = useState(null); const [selectedCase, setSelectedCase] = useState(null); const [members, setMembers] = useState([]); const [loadingList, setLoadingList] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); const [updatingTaskId, setUpdatingTaskId] = useState(null); const canEdit = canEditCases(currentOrganization); const locale = user?.language ?? 'en'; const treatmentLabel = useCallback( (type: string) => treatmentTypeLabelFromCatalog(type, treatmentCatalog), [treatmentCatalog], ); const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo( () => [ { value: 'PENDING', label: t('statusPending') }, { value: 'IN_PROGRESS', label: t('statusInProgress') }, { value: 'COMPLETED', label: t('statusCompleted') }, ], [t], ); const hasActiveFilters = Boolean( search.trim() || clinicId || treatmentType || sentFrom || sentTo, ); const loadCases = async (params: { q: string; clinicOrganizationId: string; treatmentType: string; sentFrom: string; sentTo: string; page: number; }) => { setLoadingList(true); toast.setError(''); try { const response = await casesApi.list({ q: params.q.trim() || undefined, clinicOrganizationId: params.clinicOrganizationId || undefined, treatmentType: params.treatmentType || undefined, sentFrom: params.sentFrom || undefined, sentTo: params.sentTo || undefined, page: params.page, limit: PAGE_SIZE, }); setCases(response.data.items); setPagination(response.data.pagination); } catch (error: unknown) { toast.showError(formatApiErrorMessage(error, t('errorLoadList'))); } finally { setLoadingList(false); } }; const loadDetail = async (caseId: string) => { setLoadingDetail(true); toast.setError(''); try { const response = await casesApi.getOne(caseId); setSelectedCase(response.data); } catch (error: unknown) { toast.showError(formatApiErrorMessage(error, t('errorLoadDetail'))); setSelectedCase(null); } finally { setLoadingDetail(false); } }; useEffect(() => { void casesApi.listFilterOptions().then((r) => setFilterOptions(r.data)).catch(() => {}); void casesApi.listAssignableMembers().then((r) => setMembers(r.data)).catch(() => {}); void treatmentCatalogApi.list().then((r) => setTreatmentCatalog(r.data)).catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only initial fetch }, []); useEffect(() => { const caseIdFromUrl = searchParams.get('caseId'); if (caseIdFromUrl) { setSelectedCaseId(caseIdFromUrl); } }, [searchParams]); useEffect(() => { const timeout = setTimeout(() => { void loadCases({ q: search, clinicOrganizationId: clinicId, treatmentType, sentFrom, sentTo, page, }); }, search ? 300 : 0); return () => clearTimeout(timeout); // eslint-disable-next-line react-hooks/exhaustive-deps -- debounced search + filter reload }, [search, clinicId, treatmentType, sentFrom, sentTo, page]); useEffect(() => { if (selectedCaseId) { void loadDetail(selectedCaseId); } else { setSelectedCase(null); } // eslint-disable-next-line react-hooks/exhaustive-deps -- reload when selection changes }, [selectedCaseId]); function clearFilters() { setSearch(''); setClinicId(''); setTreatmentType(''); setSentFrom(''); setSentTo(''); setPage(1); } async function handleTaskUpdate( taskId: string, payload: { assigneeUserId?: string | null; priority?: number }, ) { if (!selectedCaseId || !canEdit) return; setUpdatingTaskId(taskId); toast.setError(''); try { await casesApi.updateTask(selectedCaseId, taskId, payload); await loadDetail(selectedCaseId); await loadCases({ q: search, clinicOrganizationId: clinicId, treatmentType, sentFrom, sentTo, page, }); } catch (error: unknown) { toast.showError(formatApiErrorMessage(error, t('errorUpdateTask'))); } finally { setUpdatingTaskId(null); } } const filterSelectClass = `${FORM_SELECT_CLASS} w-full rounded-md px-3 py-2`; return (

{t('title')}

{t('subtitle')}

{ setSearch(value); setPage(1); }} placeholder={t('searchPlaceholder')} />
{hasActiveFilters ? ( ) : null}
{loadingList ? (

{tCommon('loading')}

) : cases.length === 0 ? (

{t('emptyList')}

) : (
    {cases.map((item) => { const isActive = item.id === selectedCaseId; return (
  • ); })}
)}
{pagination.totalPages > 1 ? (
{t('pageSummary', { page: pagination.page, totalPages: pagination.totalPages, total: pagination.total, })}
) : null}
{!selectedCaseId ? (

{t('selectCaseHint')}

) : loadingDetail || !selectedCase ? (

{tCommon('loading')}

) : (

{formatPatientName(selectedCase.patient)}

{t('patientMobile')}: {selectedCase.patient.mobile}

{t('fromClinic', { name: selectedCase.clinic.name })}

{t('sentAt', { date: formatDateTime(selectedCase.sentAt, locale) })}

{t('taskProgressLabel', { completed: selectedCase.taskProgress.completed, total: selectedCase.taskProgress.total, })}

{selectedCase.labComment ? (

{t('labComment')}:{' '} {selectedCase.labComment}

) : null}
{selectedCase.details.length > 0 && (

{t('treatmentDetails')}

    {selectedCase.details.map((detail) => (
  • {treatmentLabel(detail.treatmentType)}
    {t('teethLabel')}: {detail.teeth.join(', ') || '—'}
    {detail.comment ? (
    {detail.comment}
    ) : null}
  • ))}
)}

{t('tasksByTooth')}

{selectedCase.tasksByTooth.length === 0 ? (

{t('noTasks')}

) : ( selectedCase.tasksByTooth.map((group) => (
{t('toothGroupTitle', { tooth: group.tooth, prosthesis: group.prosthesisTypeLabel, type: treatmentLabel(group.treatmentType), })}
    {group.tasks.map((task) => (
  • {task.stepOrder}. {task.stepLabel} {statusOptions.find((opt) => opt.value === task.status)?.label ?? task.status}
  • ))}
)) )}
)}
); }