'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
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, type BadgeVariant } 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 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 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}%
);
}
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 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: 'PENDING', label: tCases('statusPending') },
{ 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);
return;
}
let cancelled = false;
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]);
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)}
{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,
})}
{selectedCase.labComment ? (
{tCases('labComment')}:{' '}
{selectedCase.labComment}
) : null}
{selectedCase.details.length > 0 && (
{tCases('treatmentDetails')}
)}
{tCases('tasksByTooth')}
{selectedCase.tasksByTooth.length === 0 ? (
{tCases('noTasks')}
) : (
selectedCase.tasksByTooth.map((group) => (
{tCases('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}
))}
))
)}
)}
);
}