'use client'; import { useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/shared/Button'; import { Checkbox } from '@/components/ui/shared/Checkbox'; import { Dropdown } from '@/components/ui/shared/Dropdown'; import { FORM_SELECT_CLASS } from '@/components/ui/shared/formSelectStyles'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { formatCaseSentSummary } from '@/components/treatment/caseSendLabel'; import { CaseSentLabel } from '@/components/ui/treatment/CaseSentLabel'; import { treatmentTypeLabelFromCatalog } from '@/components/ui/treatment/treatmentTypeDisplay'; import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog'; import type { ProsthesisCatalogEntry, TreatmentCatalogEntry } from '@/types/treatment-catalog'; import type { LabCaseDraft, LinkedOrganizationOption, TreatmentDetailDraft } from '@/types/treatment'; interface LabCasesDispatchPanelProps { details: TreatmentDetailDraft[]; labCases: LabCaseDraft[]; labDependentCodes: Set; treatmentCatalog: TreatmentCatalogEntry[]; activeLabCaseId: string | null; onActiveLabCaseChange: (id: string) => void; onLabCasesChange: (labCases: LabCaseDraft[]) => void; disabled: boolean; canEdit: boolean; orgs: LinkedOrganizationOption[]; organizationSearch: string; onOrganizationSearchChange: (value: string) => void; recentOrganizationIds: string[]; onRecentOrganizationPick: (orgId: string) => void; sendBusyId: string | null; onAddLabCase: () => void; onSendLabCase: (labCase: LabCaseDraft) => void; } function sentDetailClientIds(labCases: LabCaseDraft[]): Set { const ids = new Set(); for (const lc of labCases) { if (!lc.sentAt) continue; for (const id of lc.detailClientIds) ids.add(id); } return ids; } function detailInOtherDraftShipment( detailClientId: string, labCases: LabCaseDraft[], activeLabCaseClientId: string, ): boolean { return labCases.some( (lc) => !lc.sentAt && lc.clientId !== activeLabCaseClientId && lc.detailClientIds.includes(detailClientId), ); } function unsentLabDetails( details: TreatmentDetailDraft[], labCases: LabCaseDraft[], labDependentCodes: Set, ): TreatmentDetailDraft[] { const sent = sentDetailClientIds(labCases); return details.filter((d) => labDependentCodes.has(d.treatmentType) && !sent.has(d.clientId)); } function detailsAvailableForNewShipment( details: TreatmentDetailDraft[], labCases: LabCaseDraft[], labDependentCodes: Set, ): TreatmentDetailDraft[] { return unsentLabDetails(details, labCases, labDependentCodes).filter( (d) => !detailInOtherDraftShipment(d.clientId, labCases, ''), ); } function selectableDetailsForDraftShipment( details: TreatmentDetailDraft[], labCases: LabCaseDraft[], labDependentCodes: Set, activeLabCase: LabCaseDraft, ): TreatmentDetailDraft[] { const sent = sentDetailClientIds(labCases); return details.filter((d) => { if (!labDependentCodes.has(d.treatmentType)) return false; if (sent.has(d.clientId)) return false; if (activeLabCase.detailClientIds.includes(d.clientId)) return true; return !detailInOtherDraftShipment(d.clientId, labCases, activeLabCase.clientId); }); } function prosthesisTeethRows( labCase: LabCaseDraft, details: TreatmentDetailDraft[], ): Array<{ detailClientId: string; tooth: string; detailNumber: number }> { const rows: Array<{ detailClientId: string; tooth: string; detailNumber: number }> = []; for (const clientId of labCase.detailClientIds) { const detail = details.find((d) => d.clientId === clientId); if (!detail || detail.treatmentType !== 'prosthesis') continue; const detailNumber = details.findIndex((d) => d.clientId === clientId) + 1; for (const tooth of detail.teeth) { rows.push({ detailClientId: clientId, tooth, detailNumber }); } } return rows; } function isProsthesisMapComplete(labCase: LabCaseDraft, details: TreatmentDetailDraft[]): boolean { const rows = prosthesisTeethRows(labCase, details); if (rows.length === 0) return true; return rows.every((row) => labCase.toothProsthesis.some( (tp) => tp.detailClientId === row.detailClientId && tp.tooth === row.tooth && Boolean(tp.prosthesisTypeCode), ), ); } export function LabCasesDispatchPanel({ details, labCases, labDependentCodes, treatmentCatalog, activeLabCaseId, onActiveLabCaseChange, onLabCasesChange, disabled, canEdit, orgs, organizationSearch, onOrganizationSearchChange, recentOrganizationIds, onRecentOrganizationPick, sendBusyId, onAddLabCase, onSendLabCase, }: LabCasesDispatchPanelProps) { const t = useTranslations('treatment'); const [prosthesisOptions, setProsthesisOptions] = useState([]); const [applyAllProsthesis, setApplyAllProsthesis] = useState(''); const activeLinkedOrganizations = orgs.filter((o) => o.active); const filteredOrganizations = (() => { const q = organizationSearch.trim().toLowerCase(); if (!q) return activeLinkedOrganizations; return activeLinkedOrganizations.filter((o) => o.name.toLowerCase().includes(q)); })(); const recentOrganizations = recentOrganizationIds .map((id) => activeLinkedOrganizations.find((o) => o.id === id)) .filter(Boolean) as LinkedOrganizationOption[]; const labEligibleDetails = useMemo( () => details.filter((d) => labDependentCodes.has(d.treatmentType)), [details, labDependentCodes], ); const canAddLabShipment = useMemo( () => detailsAvailableForNewShipment(details, labCases, labDependentCodes).length > 0, [details, labCases, labDependentCodes], ); const activeLabCase = labCases.find((lc) => lc.clientId === activeLabCaseId) ?? labCases[0] ?? null; const sent = Boolean(activeLabCase?.sentAt); const activeLabOrgName = activeLabCase?.destinationOrganizationId ? orgs.find((o) => o.id === activeLabCase.destinationOrganizationId)?.name : null; const prosthesisRows = activeLabCase ? prosthesisTeethRows(activeLabCase, details) : []; const prosthesisComplete = activeLabCase ? isProsthesisMapComplete(activeLabCase, details) : true; useEffect(() => { if (!activeLabCase?.destinationOrganizationId) { setProsthesisOptions([]); return; } let cancelled = false; void prosthesisCatalogApi .list(activeLabCase.destinationOrganizationId) .then((res) => { if (!cancelled) setProsthesisOptions(res.data); }) .catch(() => { if (!cancelled) setProsthesisOptions([]); }); return () => { cancelled = true; }; }, [activeLabCase?.destinationOrganizationId]); function detailNumber(d: TreatmentDetailDraft) { const idx = details.findIndex((row) => row.clientId === d.clientId); return idx >= 0 ? idx + 1 : 0; } function detailSummary(d: TreatmentDetailDraft) { const typeLabel = treatmentTypeLabelFromCatalog(d.treatmentType, treatmentCatalog); const teeth = d.teeth.length ? d.teeth.join(', ') : t('teethNone'); return `${t('detailLabel', { n: detailNumber(d) })} · ${typeLabel} · ${teeth}`; } function updateActiveLabCase(patch: Partial) { if (!activeLabCase) return; onLabCasesChange( labCases.map((lc) => (lc.clientId === activeLabCase.clientId ? { ...lc, ...patch } : lc)), ); } function setToothProsthesis( detailClientId: string, tooth: string, prosthesisTypeCode: string, ) { if (!activeLabCase) return; const rest = activeLabCase.toothProsthesis.filter( (tp) => !(tp.detailClientId === detailClientId && tp.tooth === tooth), ); const next = prosthesisTypeCode ? [...rest, { detailClientId, tooth, prosthesisTypeCode }] : rest; updateActiveLabCase({ toothProsthesis: next }); } function applyProsthesisToAll(code: string) { if (!activeLabCase || !code) return; const next = prosthesisRows.map((row) => ({ detailClientId: row.detailClientId, tooth: row.tooth, prosthesisTypeCode: code, })); updateActiveLabCase({ toothProsthesis: next }); } function toggleDetailInActiveLabCase(detailClientId: string, checked: boolean) { if (!activeLabCase || sent) return; onLabCasesChange( labCases.map((lc) => { if (lc.sentAt) return lc; if (lc.clientId === activeLabCase.clientId) { const set = new Set(lc.detailClientIds); if (checked) set.add(detailClientId); else set.delete(detailClientId); const keptProsthesis = lc.toothProsthesis.filter((tp) => [...set].includes(tp.detailClientId), ); return { ...lc, detailClientIds: [...set], toothProsthesis: keptProsthesis }; } if (checked) { return { ...lc, detailClientIds: lc.detailClientIds.filter((id) => id !== detailClientId), }; } return lc; }), ); } if (labEligibleDetails.length === 0) { return (

{t('labDispatchTitle')}

{t('noLabDetails')}

); } const includedInActiveShipment = activeLabCase ? labEligibleDetails.filter((d) => activeLabCase.detailClientIds.includes(d.clientId)) : []; const pickableForActiveDraft = activeLabCase && !sent ? selectableDetailsForDraftShipment(details, labCases, labDependentCodes, activeLabCase) : []; return (

{t('labDispatchTitle')}

{t('labDispatchSubtitle')} {t('labDispatchSendHint')}

{canAddLabShipment && ( )}
{labCases.length === 0 ? (

{t('labDispatchEmpty')}

) : ( <>
{labCases.map((lc, idx) => { const sentSummary = formatCaseSentSummary( lc.sends, { organizationIds: lc.destinationOrganizationId ? [lc.destinationOrganizationId] : [], sentAt: lc.sentAt ?? null, orgs, }, t, ); return ( ); })}
{activeLabCase && (
{sent ? ( <>

{t('labShipmentIncludedDetails')}

{includedInActiveShipment.length === 0 ? (

{t('labShipmentNoIncludedDetails')}

) : (
    {includedInActiveShipment.map((d) => (
  • {detailSummary(d)}
  • ))}
)}
{activeLabCase.labComment.trim() ? (

{t('labComment')}

{activeLabCase.labComment}

) : null} {activeLabOrgName ? (

{t('selectLab')}

{activeLabOrgName}

) : null} ) : ( <>

{t('includeDetails')}

{pickableForActiveDraft.length === 0 ? (

{t('labShipmentNoDetailsAvailable')}

) : (
{pickableForActiveDraft.map((d) => { const checked = activeLabCase.detailClientIds.includes(d.clientId); return ( toggleDetailInActiveLabCase(d.clientId, next)} label={detailSummary(d)} /> ); })}
)}