'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('tasksByTooth')}
{selectedCase.tasksByTooth.length === 0 ? (
{t('noTasks')}
) : (
selectedCase.tasksByTooth.map((group, groupIndex) => (
{group.prosthesisTypeLabel}
{t('toothGroupTitle', {
teeth: formatToothList(group.teeth),
prosthesis: group.prosthesisTypeLabel,
})}
))
)}
{selectedCaseId ? (
) : null}
)}
);
}