'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { MessageSquare } from 'lucide-react'; import { formatApiErrorMessage } from '@/components/shared/formatApiError'; import { useAuth } from '@/lib/hooks/useAuth'; import { useToast } from '@/lib/hooks/useToast'; import { organizationApi } from '@/lib/api/organization'; import { treatmentCatalogApi } from '@/lib/api/treatment-catalog'; import { treatmentTypeLabelFromCatalog } from '@/components/ui/treatment/treatmentTypeDisplay'; import { Badge } from '@/components/ui/shared/Badge'; import { Button } from '@/components/ui/shared/Button'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { ToastStack } from '@/components/ui/shared/Toast'; import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel'; import { CaseToothChartPanel } from '@/components/ui/lab/CaseToothChartPanel'; import { LabCaseAttachmentPreview } from '@/components/ui/lab/LabCaseAttachmentPreview'; import { labTaskStatusVariant } from '@/components/ui/lab/labTaskStatusDisplay'; import { formatToothList, prosthesisTypeBadgeStyle, } from '@/components/ui/treatment/prosthesisTypeDisplay'; import { treatmentsApi } from '@/lib/api/treatments'; import type { CounterpartItemDto } from '@/lib/api/organization'; import type { LabCaseDetail, LabCaseListItem, LabTaskStatus } from '@/types/cases'; import type { TreatmentCatalogEntry } from '@/types/treatment-catalog'; 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}%
); } interface ConnectionCaseHistoryContentProps { connection: CounterpartItemDto; onBack: () => void; } export function ConnectionCaseHistoryContent({ connection, onBack, }: ConnectionCaseHistoryContentProps) { const t = useTranslations('organizations'); const tCases = useTranslations('cases'); const tCommon = useTranslations('common'); const { currentOrganization, user } = useAuth(); const { showError, setError, messages: toastMessages } = useToast(); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [cases, setCases] = useState([]); const [pagination, setPagination] = useState({ page: 1, limit: PAGE_SIZE, total: 0, totalPages: 1, }); const [selectedCaseId, setSelectedCaseId] = useState(null); const [selectedCase, setSelectedCase] = useState(null); const [treatmentCatalog, setTreatmentCatalog] = useState([]); const [loadingList, setLoadingList] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); const [commentCount, setCommentCount] = useState(0); const locale = user?.language ?? 'en'; const isClinic = currentOrganization?.type === 'CLINIC'; const tRef = useRef(t); tRef.current = t; const treatmentLabel = useCallback( (type: string) => treatmentTypeLabelFromCatalog(type, treatmentCatalog), [treatmentCatalog], ); useEffect(() => { void treatmentCatalogApi.list().then((r) => setTreatmentCatalog(r.data)).catch(() => {}); }, []); const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo( () => [ { value: 'IN_PROGRESS', label: tCases('statusInProgress') }, { value: 'COMPLETED', label: tCases('statusCompleted') }, ], [tCases], ); useEffect(() => { let cancelled = false; const timeout = setTimeout(() => { void (async () => { setLoadingList(true); setError(''); try { const response = await organizationApi.listConnectionCases(connection.id, { q: search.trim() || undefined, page, limit: PAGE_SIZE, }); if (cancelled) return; setCases(response.data.items); setPagination(response.data.pagination); } catch (error: unknown) { if (cancelled) return; showError(formatApiErrorMessage(error, tRef.current('caseHistoryErrorLoadList'))); } finally { if (!cancelled) setLoadingList(false); } })(); }, search ? 300 : 0); return () => { cancelled = true; clearTimeout(timeout); }; }, [search, page, connection.id, showError, setError]); useEffect(() => { if (!selectedCaseId) { setSelectedCase(null); setCommentCount(0); return; } let cancelled = false; void organizationApi .listConnectionCaseComments(connection.id, selectedCaseId) .then((r) => { if (!cancelled) setCommentCount(r.data.length); }) .catch(() => { if (!cancelled) setCommentCount(0); }); void (async () => { setLoadingDetail(true); setError(''); try { const response = await organizationApi.getConnectionCase(connection.id, selectedCaseId); if (cancelled) return; setSelectedCase(response.data); } catch (error: unknown) { if (cancelled) return; showError(formatApiErrorMessage(error, tRef.current('caseHistoryErrorLoadDetail'))); setSelectedCase(null); } finally { if (!cancelled) setLoadingDetail(false); } })(); return () => { cancelled = true; }; }, [selectedCaseId, connection.id, showError, setError]); function scrollToComments() { document.getElementById('case-comments')?.scrollIntoView({ behavior: 'smooth' }); } const loadClinicAttachmentBlob = useCallback( (_caseId: string, attachmentId: string) => treatmentsApi.getAttachmentFileBlob(attachmentId), [], ); const latestCaseAttachment = useMemo(() => { if (!selectedCase?.attachments.length) return null; return [...selectedCase.attachments].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), )[0]; }, [selectedCase?.attachments]); const caseProsthesisRows = useMemo(() => { if (!selectedCase) return []; if (selectedCase.toothProsthesis.length > 0) { const byCode = new Map(); for (const row of selectedCase.toothProsthesis) { const key = row.prosthesisTypeCode; const teeth = byCode.get(key) ?? []; if (!teeth.includes(row.tooth)) teeth.push(row.tooth); byCode.set(key, teeth); } return [...byCode.entries()].map(([prosthesisTypeCode, teeth]) => ({ prosthesisTypeCode, teeth, })); } return selectedCase.tasksByTooth.map((g) => ({ prosthesisTypeCode: g.prosthesisTypeCode, teeth: g.teeth, })); }, [selectedCase]); return (

{t('caseHistoryTitle', { name: connection.organizationName })}

{isClinic ? t('caseHistorySubtitleClinic') : t('caseHistorySubtitleLab')}

{ setSearch(value); setPage(1); }} placeholder={tCases('searchPlaceholder')} />
{loadingList ? (

{tCommon('loading')}

) : cases.length === 0 ? (

{t('caseHistoryEmpty')}

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

{tCases('selectCaseHint')}

) : loadingDetail || !selectedCase ? (

{tCommon('loading')}

) : (

{formatPatientName(selectedCase.patient)}

{isClinic ? ( ) : null}

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

{!isClinic ? (

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

) : (

{t('caseHistorySentToLab', { name: connection.organizationName })}

)}

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

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

{latestCaseAttachment && selectedCaseId ? (

{tCases('latestAttachment')}

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

{tCases('treatmentDetails')}

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

{tCases('tasksByTooth')}

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

{tCases('noTasks')}

) : ( selectedCase.tasksByTooth.map((group, groupIndex) => (
{group.prosthesisTypeLabel} {tCases('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} {task.lastStatusChangedBy ? ( {tCases('lastUpdatedBy', { name: task.lastStatusChangedBy.name })} ) : null}
  • ))}
)) )}
{isClinic && selectedCaseId ? (
{ const r = await organizationApi.listConnectionCaseComments( connection.id, selectedCaseId, ); setCommentCount(r.data.length); return r.data; }} onPost={async (body) => { const r = await organizationApi.addConnectionCaseComment( connection.id, selectedCaseId, body, ); setCommentCount((n) => n + 1); return r.data; }} onError={showError} />
) : null}
)}
); }