'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { MessageSquare } from 'lucide-react'; 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, canEditTasks } from '@/components/shared/permissions'; import { Badge } from '@/components/ui/shared/Badge'; import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel'; import { labTaskStatusVariant } from '@/components/ui/lab/labTaskStatusDisplay'; import { casesApi } from '@/lib/api/cases'; import { tasksApi } from '@/lib/api/tasks'; 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 { CasesFilterOptions, LabCaseDetail, LabCaseListItem, LabTaskStatus, PaginatedLabCases, } from '@/types/cases'; import { formatToothList, prosthesisTypeBadgeStyle, } from '@/components/ui/treatment/prosthesisTypeDisplay'; const PAGE_SIZE = 20; 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 [loadingList, setLoadingList] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); const [updatingTaskId, setUpdatingTaskId] = useState(null); const [commentCount, setCommentCount] = useState(0); const canEdit = canEditCases(currentOrganization); const canEditComments = canEditTasks(currentOrganization); const locale = user?.language ?? 'en'; const treatmentLabel = useCallback( (type: string) => treatmentTypeLabelFromCatalog(type, treatmentCatalog), [treatmentCatalog], ); const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo( () => [ { 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 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); void tasksApi .listComments(selectedCaseId) .then((r) => setCommentCount(r.data.length)) .catch(() => setCommentCount(0)); } else { setSelectedCase(null); setCommentCount(0); } // eslint-disable-next-line react-hooks/exhaustive-deps -- reload when selection changes }, [selectedCaseId]); function scrollToComments() { document.getElementById('case-comments')?.scrollIntoView({ behavior: 'smooth' }); } function clearFilters() { setSearch(''); setClinicId(''); setTreatmentType(''); setSentFrom(''); setSentTo(''); setPage(1); } async function handleImportantToggle(taskId: string, isImportant: boolean) { if (!selectedCaseId || !canEdit) return; setUpdatingTaskId(taskId); toast.setError(''); try { await casesApi.setTaskImportant(selectedCaseId, taskId, isImportant); await loadDetail(selectedCaseId); } 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.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, groupIndex) => (
{group.prosthesisTypeLabel} {t('toothGroupTitle', { teeth: formatToothList(group.teeth), prosthesis: group.prosthesisTypeLabel, })}
    {group.tasks.map((task) => (
  • {task.stepOrder}. {task.stepLabel} {statusOptions.find((opt) => opt.value === task.status)?.label ?? task.status} {canEdit ? ( ) : task.isImportant ? ( {t('importantLabel')} ) : null}

    {task.lastStatusChangedBy ? t('lastUpdatedBy', { name: task.lastStatusChangedBy.name }) : t('lastUpdatedUnknown')} {task.lastStatusChangedAt ? ` · ${formatDateTime(task.lastStatusChangedAt, locale)}` : ''}

  • ))}
)) )}
{selectedCaseId ? (
{ const r = await tasksApi.listComments(selectedCaseId); setCommentCount(r.data.length); return r.data; }} onPost={async (body, visibleToClinic) => { const r = await tasksApi.addComment(selectedCaseId, { body, visibleToClinic, }); setCommentCount((n) => n + 1); return r.data; }} onToggleVisibility={async (commentId, visible) => { const r = await tasksApi.setCommentVisibility(commentId, visible); return r.data; }} onError={toast.showError} />
) : null}
)}
); }