'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AppointmentsStrip } from '@/components/treatment/AppointmentsStrip'; import { FdiToothChart } from '@/components/treatment/FdiToothChart'; import { PastTreatmentsPanel } from '@/components/treatment/PastTreatmentsPanel'; import { Checkbox } from '@/components/ui/common/Checkbox'; import { Button } from '@/components/ui/common/Button'; import { isSameLocalCalendarDay, startOfLocalDay } from '@/lib/appointmentTime'; import { fetchLinkedOrganizations, fetchMyAppointmentsForDay, fetchPastTreatments, saveTreatmentDraft, sendTreatmentRecord, } from '@/lib/mocks/treatmentMockApi'; import { pickAutoAppointment } from '@/lib/treatmentSelection'; import { canEditTreatment } from '@/shared/permissions'; import type { Organization } from '@/types/organization'; import type { FdiToothId, LinkedOrganizationOption, PastTreatment, TreatmentAppointment, TreatmentAttachmentMeta, TreatmentRecordDraft, } from '@/types/treatment'; function newRecord(): TreatmentRecordDraft { return { clientId: typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `rec-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, teeth: [], comment: '', attachmentMetas: [], sendToOrganizationIds: [], sentAt: null, }; } interface TreatmentWorkspaceProps { userId: string; currentOrganization: Organization | null; } export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWorkspaceProps) { const canEdit = canEditTreatment(currentOrganization); const [stripHidden, setStripHidden] = useState(false); const scheduleMinDate = useMemo(() => startOfLocalDay(new Date()), []); const [selectedDay, setSelectedDay] = useState(() => startOfLocalDay(new Date())); const [appointments, setAppointments] = useState([]); const [apptsLoading, setApptsLoading] = useState(false); const [selectionLocked, setSelectionLocked] = useState(false); const [selectedAppointmentId, setSelectedAppointmentId] = useState(null); const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [orgs, setOrgs] = useState([]); const [records, setRecords] = useState(() => [newRecord()]); const [activeRecordId, setActiveRecordId] = useState(() => records[0].clientId); const selectionLockedRef = useRef(selectionLocked); selectionLockedRef.current = selectionLocked; const attachmentInputRef = useRef(null); const [saveBusy, setSaveBusy] = useState(false); const [sendBusyId, setSendBusyId] = useState(null); const [banner, setBanner] = useState(null); const selectedAppointment = useMemo( () => appointments.find((a) => a.id === selectedAppointmentId) ?? null, [appointments, selectedAppointmentId], ); const activeRecord = useMemo( () => records.find((r) => r.clientId === activeRecordId) ?? records[0], [records, activeRecordId], ); const selectedTeethSet = useMemo(() => new Set(activeRecord.teeth), [activeRecord.teeth]); useEffect(() => { setSelectionLocked(false); }, [selectedDay]); useEffect(() => { let cancelled = false; setApptsLoading(true); void (async () => { try { const list = await fetchMyAppointmentsForDay(userId, selectedDay); if (cancelled) return; setAppointments(list); if (!selectionLockedRef.current) { setSelectedAppointmentId(pickAutoAppointment(list, selectedDay)); } } finally { if (!cancelled) setApptsLoading(false); } })(); return () => { cancelled = true; }; }, [userId, selectedDay]); useEffect(() => { const today = startOfLocalDay(new Date()); if (!isSameLocalCalendarDay(selectedDay, today) || selectionLocked) return; const id = window.setInterval(() => { setSelectedAppointmentId((prev) => { const next = pickAutoAppointment(appointments, selectedDay); return next ?? prev; }); }, 60_000); return () => window.clearInterval(id); }, [selectedDay, appointments, selectionLocked]); useEffect(() => { let cancelled = false; void (async () => { const list = await fetchLinkedOrganizations(); if (!cancelled) setOrgs(list); })(); return () => { cancelled = true; }; }, []); useEffect(() => { if (!selectedAppointment) { setHistory([]); return; } let cancelled = false; setHistoryLoading(true); void (async () => { const items = await fetchPastTreatments(selectedAppointment.patientId); if (!cancelled) { setHistory(items); setHistoryLoading(false); } })(); return () => { cancelled = true; }; }, [selectedAppointment?.patientId]); useEffect(() => { const first = newRecord(); setRecords([first]); setActiveRecordId(first.clientId); }, [selectedAppointment?.id]); const fixActiveAfterRecordsChange = useCallback((next: TreatmentRecordDraft[]) => { setRecords(next); setActiveRecordId((id) => (next.some((r) => r.clientId === id) ? id : next[0].clientId)); }, []); const toggleTooth = useCallback( (fdi: FdiToothId) => { if (!canEdit) return; setRecords((prev) => prev.map((r) => { if (r.clientId !== activeRecordId) return r; const set = new Set(r.teeth); if (set.has(fdi)) set.delete(fdi); else set.add(fdi); return { ...r, teeth: [...set].sort() as FdiToothId[] }; }), ); }, [activeRecordId, canEdit], ); const onPickAppointment = useCallback((id: string) => { setSelectionLocked(true); setSelectedAppointmentId(id); }, []); const addAttachments = useCallback( (files: FileList | null) => { if (!files?.length || !canEdit) return; setRecords((prev) => prev.map((r) => { if (r.clientId !== activeRecordId) return r; const added: TreatmentAttachmentMeta[] = [...r.attachmentMetas]; for (let i = 0; i < files.length; i++) { const f = files[i]; added.push({ id: `local-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 7)}`, fileName: f.name, mimeType: f.type || 'application/octet-stream', sizeBytes: f.size, }); } return { ...r, attachmentMetas: added }; }), ); }, [activeRecordId, canEdit], ); const handleSaveAll = useCallback(async () => { if (!canEdit || !selectedAppointment) return; setSaveBusy(true); setBanner(null); try { await saveTreatmentDraft({ appointmentId: selectedAppointment.id, patientId: selectedAppointment.patientId, records: records.map(({ clientId: _c, sentAt: _s, ...rest }) => rest), }); setBanner('Treatment draft saved (mock). You can send records later.'); } finally { setSaveBusy(false); } }, [canEdit, selectedAppointment, records]); const handleSendRecord = useCallback( async (record: TreatmentRecordDraft) => { if (!canEdit || !selectedAppointment) return; const targets = record.sendToOrganizationIds.filter((id) => orgs.some((o) => o.id === id && o.active), ); if (targets.length === 0) { setBanner('Choose at least one active organization to send this record.'); return; } setSendBusyId(record.clientId); setBanner(null); try { await sendTreatmentRecord({ appointmentId: selectedAppointment.id, patientId: selectedAppointment.patientId, recordClientId: record.clientId, organizationIds: targets, }); setRecords((prev) => prev.map((r) => r.clientId === record.clientId ? { ...r, sentAt: new Date().toISOString() } : r, ), ); setBanner('Record sent to selected organizations (mock).'); } finally { setSendBusyId(null); } }, [canEdit, selectedAppointment, orgs], ); if (!canEdit) { return (

Treatment workspace

Your role can view the Treatment tab, but editing clinical workflows requires{' '} Treatment edit permission.

); } return (

Treatment

Mocked data — appointments, history, save, and send are simulated until backend endpoints exist.

setStripHidden((s) => !s)} selectedDay={selectedDay} onSelectDay={setSelectedDay} minScheduleDate={scheduleMinDate} appointments={appointments} selectedAppointmentId={selectedAppointmentId} onSelectAppointment={onPickAppointment} loading={apptsLoading} /> {banner && (
{banner}
)}
{selectedAppointment ? (

Selected patient

{selectedAppointment.patientFirstName} {selectedAppointment.patientLastName}

Appointment purpose:{' '} {selectedAppointment.purpose}

) : (
{apptsLoading ? 'Loading appointments…' : 'Select a day with at least one appointment.'}
)}

Treatment records

Each record has its own teeth, notes, attachments, and destinations for send.

{records.map((r, idx) => ( ))}
{activeRecord && (