'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip'; import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart'; import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel'; import { Checkbox } from '@/components/ui/shared/Checkbox'; import { Button } from '@/components/ui/shared/Button'; import { Dropdown } from '@/components/ui/shared/Dropdown'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { Toast } from '@/components/ui/shared/Toast'; import { compareLocalDayStart, isSameLocalCalendarDay, startOfLocalDay } from '@/components/appointments/appointmentTime'; import { fetchLinkedOrganizations, fetchMyAppointmentsForDay, fetchPastTreatments, saveTreatmentDraft, sendTreatmentRecord, } from '@/lib/mocks/treatmentMockApi'; import { pickAutoAppointment } from '@/components/shared/treatmentSelection'; import { canEditTreatment } from '@/components/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)}`, treatmentType: 'consultation', 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 todayStart = 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 [organizationSearch, setOrganizationSearch] = useState(''); const [recentOrganizationIds, setRecentOrganizationIds] = useState([]); const [reviewTreatment, setReviewTreatment] = useState(null); const selectedAppointment = useMemo( () => appointments.find((a) => a.id === selectedAppointmentId) ?? null, [appointments, selectedAppointmentId], ); const isViewingPastDay = useMemo( () => compareLocalDayStart(selectedDay, todayStart) < 0, [selectedDay, todayStart], ); const canEditTreatmentForDay = Boolean(selectedAppointment) && !isViewingPastDay; const activeRecord = useMemo( () => records.find((r) => r.clientId === activeRecordId) ?? records[0], [records, activeRecordId], ); const selectedTeethSet = useMemo(() => new Set(activeRecord.teeth), [activeRecord.teeth]); const activeLinkedOrganizations = useMemo(() => orgs.filter((o) => o.active), [orgs]); const filteredOrganizations = useMemo(() => { const q = organizationSearch.trim().toLowerCase(); if (!q) return activeLinkedOrganizations; return activeLinkedOrganizations.filter((o) => o.name.toLowerCase().includes(q)); }, [organizationSearch, activeLinkedOrganizations]); const recentOrganizations = useMemo(() => { if (recentOrganizationIds.length === 0) return []; const recentSet = new Set(recentOrganizationIds); return activeLinkedOrganizations .filter((o) => recentSet.has(o.id)) .sort((a, b) => recentOrganizationIds.indexOf(a.id) - recentOrganizationIds.indexOf(b.id)) .slice(0, 3); }, [recentOrganizationIds, activeLinkedOrganizations]); const currentDraftPreview = useMemo(() => { if (!selectedAppointment) return null; return { id: 'current-draft', patientId: selectedAppointment.patientId, title: `Current draft for ${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`, treatmentAt: new Date().toISOString(), status: 'draft', records: records.map((r, idx) => ({ id: r.clientId || `draft-${idx + 1}`, treatmentType: r.treatmentType, teeth: r.teeth, notes: r.comment || null, })), documents: records.flatMap((r) => r.attachmentMetas), }; }, [records, selectedAppointment]); const treatmentTypeTextColor = useMemo(() => { const map: Record = { consultation: '#ddd6fe', filling: '#fed7aa', endo: '#fecaca', visit: '#bae6fd', hygiene: '#d9f99d', }; return map[activeRecord.treatmentType]; }, [activeRecord.treatmentType]); 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); setOrganizationSearch(''); setReviewTreatment(null); }, [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, ), ); setRecentOrganizationIds((prev) => { const next = [...record.sendToOrganizationIds.filter((id) => id && !prev.includes(id)), ...prev]; return next.slice(0, 10); }); 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} appointments={appointments} selectedAppointmentId={selectedAppointmentId} onSelectAppointment={onPickAppointment} loading={apptsLoading} /> {isViewingPastDay && (

Past days are view-only. You can review appointments and history, but treatment records cannot be added or changed.

)}
{selectedAppointment ? (

Selected patient

{selectedAppointment.patientFirstName} {selectedAppointment.patientLastName}

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

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

Treatment review

{!reviewTreatment && (

Select a treatment from history, or preview the current draft.

)} {reviewTreatment && (

{reviewTreatment.title}

{new Date(reviewTreatment.treatmentAt).toLocaleDateString()}

Status: {reviewTreatment.status}

{reviewTreatment.records.map((r, idx) => (

Record {idx + 1}

Type: {r.treatmentType}

Teeth: {r.teeth.length ? [...r.teeth].sort().join(', ') : 'None selected'}

{r.notes &&

Notes: {r.notes}

}
))}

Attachments: {reviewTreatment.documents.length}

)}

Treatment records

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

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