'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; 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 { hasPermission } from '@/components/shared/permissions'; import { casesApi } from '@/lib/api/cases'; import type { AssignableMember, LabCaseDetail, LabCaseListItem, LabTaskStatus } from '@/types/cases'; const TREATMENT_TYPE_KEYS = { consultation: 'typeConsultation', filling: 'typeFilling', endo: 'typeEndo', visit: 'typeVisit', hygiene: 'typeHygiene', } as const; 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)); } export default function CasesPage() { const t = useTranslations('cases'); const tTreatment = useTranslations('treatment'); const tCommon = useTranslations('common'); const { currentOrganization, user } = useAuth(); const toast = useToast(); const [search, setSearch] = useState(''); const [cases, setCases] = 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 = hasPermission(currentOrganization, 'TAB_CASES_EDIT'); const locale = user?.language ?? 'en'; const treatmentLabel = useCallback( (type: string) => { const key = TREATMENT_TYPE_KEYS[type as keyof typeof TREATMENT_TYPE_KEYS]; return key ? tTreatment(key) : type; }, [tTreatment], ); 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 loadCases = async (q: string) => { setLoadingList(true); toast.setError(''); try { const response = await casesApi.list({ q: q.trim() || undefined, page: 1, limit: 50 }); setCases(response.data.items); } 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 loadCases(''); void casesApi.listAssignableMembers().then((r) => setMembers(r.data)).catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only initial fetch }, []); useEffect(() => { const timeout = setTimeout(() => { void loadCases(search); }, 300); return () => clearTimeout(timeout); // eslint-disable-next-line react-hooks/exhaustive-deps -- debounced search only }, [search]); useEffect(() => { if (selectedCaseId) { void loadDetail(selectedCaseId); } else { setSelectedCase(null); } // eslint-disable-next-line react-hooks/exhaustive-deps -- reload when selection changes }, [selectedCaseId]); async function handleTaskUpdate( taskId: string, payload: { assigneeUserId?: string | null; status?: LabTaskStatus }, ) { if (!selectedCaseId || !canEdit) return; setUpdatingTaskId(taskId); toast.setError(''); try { await casesApi.updateTask(selectedCaseId, taskId, payload); await loadDetail(selectedCaseId); await loadCases(search); } catch (error: unknown) { toast.showError(formatApiErrorMessage(error, t('errorUpdateTask'))); } finally { setUpdatingTaskId(null); } } return (

{t('title')}

{t('subtitle')}

setSearch(e.target.value)} placeholder={t('searchPlaceholder')} className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm" /> {loadingList ? (

{tCommon('loading')}

) : cases.length === 0 ? (

{t('emptyList')}

) : (
    {cases.map((item) => { const isActive = item.id === selectedCaseId; const progress = item.taskProgress.total > 0 ? `${item.taskProgress.completed}/${item.taskProgress.total}` : '0/0'; return (
  • ); })}
)}
{!selectedCaseId ? (

{t('selectCaseHint')}

) : loadingDetail || !selectedCase ? (

{tCommon('loading')}

) : (

{formatPatientName(selectedCase.patient)}

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

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

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

{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, type: treatmentLabel(group.treatmentType), })}
    {group.tasks.map((task) => (
  • {task.stepOrder}. {task.stepLabel}
  • ))}
)) )}
)}
); }