From c2d490a5e759e28917defd0e4092b6d0bf0cd520 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 7 May 2026 14:20:50 +0330 Subject: [PATCH] feature: Preview added for treatments. --- .../appointments/AppointmentBookingModal.tsx | 33 +++- .../ui/treatment/PastTreatmentsPanel.tsx | 29 ++- .../ui/treatment/TreatmentWorkspace.tsx | 168 +++++++++++++++++- frontend/src/lib/mocks/treatmentMockApi.ts | 4 +- frontend/src/types/treatment.ts | 12 ++ 5 files changed, 234 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx b/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx index 0c9a8ac..d5f4903 100644 --- a/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx +++ b/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { X } from 'lucide-react'; import { Button } from '@/components/ui/common/Button'; import { Dropdown } from '@/components/ui/common/Dropdown'; -import { APPOINTMENT_PURPOSES, type AppointmentPurpose } from '@/types/appointment'; +import type { AppointmentPurpose } from '@/types/appointment'; import { APPOINTMENT_PURPOSE_LABEL } from '@/components/ui/appointments/appointmentPurposeStyles'; import type { Patient } from '@/types/patient'; import { @@ -46,6 +46,16 @@ export function AppointmentBookingModal({ const [endTime, setEndTime] = useState('10:00'); const [purpose, setPurpose] = useState('consultation'); const [error, setError] = useState(''); + const purposeTextColor = + purpose === 'consultation' + ? '#ddd6fe' + : purpose === 'filling' + ? '#fed7aa' + : purpose === 'endo' + ? '#fecaca' + : purpose === 'visit' + ? '#bae6fd' + : '#d9f99d'; useEffect(() => { if (!open) { @@ -175,12 +185,23 @@ export function AppointmentBookingModal({ label="Purpose" value={purpose} onChange={(e) => setPurpose(e.target.value as AppointmentPurpose)} + style={{ color: purposeTextColor }} > - {APPOINTMENT_PURPOSES.map((p) => ( - - ))} + + + + + {error &&

{error}

} diff --git a/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx b/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx index 1e2dd6c..1c6c497 100644 --- a/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx +++ b/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx @@ -6,9 +6,16 @@ import type { PastTreatment } from '@/types/treatment'; interface PastTreatmentsPanelProps { items: PastTreatment[]; loading?: boolean; + selectedTreatmentId?: string | null; + onSelectTreatment?: (treatment: PastTreatment) => void; } -export function PastTreatmentsPanel({ items, loading }: PastTreatmentsPanelProps) { +export function PastTreatmentsPanel({ + items, + loading, + selectedTreatmentId, + onSelectTreatment, +}: PastTreatmentsPanelProps) { return (
@@ -28,7 +35,11 @@ export function PastTreatmentsPanel({ items, loading }: PastTreatmentsPanelProps {items.map((t) => (

{t.title}

@@ -43,6 +54,8 @@ export function PastTreatmentsPanel({ items, loading }: PastTreatmentsPanelProps {t.records.map((r) => (
  • Record: + {r.treatmentType} + {' | '} {r.teeth.length > 0 ? `Teeth ${[...r.teeth].sort().join(', ')}` : 'No teeth tagged'} {r.notes ? ` — ${r.notes}` : ''}
  • @@ -50,6 +63,18 @@ export function PastTreatmentsPanel({ items, loading }: PastTreatmentsPanelProps )} + {onSelectTreatment && ( +
    + +
    + )} + {t.documents.length > 0 && (

    Attachments

    diff --git a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx index b248784..e18ce0f 100644 --- a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx +++ b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx @@ -6,6 +6,8 @@ import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart'; import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel'; import { Checkbox } from '@/components/ui/common/Checkbox'; import { Button } from '@/components/ui/common/Button'; +import { Dropdown } from '@/components/ui/common/Dropdown'; +import { SearchBar } from '@/components/ui/common/SearchBar'; import { isSameLocalCalendarDay, startOfLocalDay } from '@/lib/appointmentTime'; import { fetchLinkedOrganizations, @@ -32,6 +34,7 @@ function newRecord(): TreatmentRecordDraft { typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `rec-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + treatmentType: 'consultation', teeth: [], comment: '', attachmentMetas: [], @@ -74,6 +77,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor 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, @@ -86,6 +92,48 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor ); 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); @@ -159,6 +207,8 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor const first = newRecord(); setRecords([first]); setActiveRecordId(first.clientId); + setOrganizationSearch(''); + setReviewTreatment(null); }, [selectedAppointment?.id]); const fixActiveAfterRecordsChange = useCallback((next: TreatmentRecordDraft[]) => { @@ -250,6 +300,10 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor 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); @@ -316,7 +370,58 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
    )} - + +
    +
    +

    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} +

    +
    + )} +
    @@ -391,6 +496,30 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor /> +
    + { + const nextType = e.target.value as TreatmentRecordDraft['treatmentType']; + setRecords((prev) => + prev.map((r) => + r.clientId === activeRecordId ? { ...r, treatmentType: nextType } : r, + ), + ); + }} + disabled={!selectedAppointment} + className="capitalize" + style={{ color: treatmentTypeTextColor }} + > + + + + + + +
    +

    Attachments (mock)

    Send this record to linked organizations

    +
    + + {recentOrganizations.length > 0 && ( +
    + Recent: + {recentOrganizations.map((o) => ( + + ))} +
    + )} +
    - {orgs.map((o) => ( + {filteredOrganizations.map((o) => ( { setRecords((prev) => prev.map((r) => { @@ -450,6 +609,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor label={`${o.name}${o.active ? '' : ' (inactive)'}`} /> ))} + {filteredOrganizations.length === 0 && ( +

    No active organization matches your search.

    + )}
    diff --git a/frontend/src/lib/mocks/treatmentMockApi.ts b/frontend/src/lib/mocks/treatmentMockApi.ts index a446da9..21dbff9 100644 --- a/frontend/src/lib/mocks/treatmentMockApi.ts +++ b/frontend/src/lib/mocks/treatmentMockApi.ts @@ -108,6 +108,7 @@ const MOCK_HISTORY: Record = { records: [ { id: 'ptr-1', + treatmentType: 'endo', teeth: ['45'] as FdiToothId[], notes: 'Instrumented and temporized.', }, @@ -136,6 +137,7 @@ const MOCK_HISTORY: Record = { records: [ { id: 'ptr-2', + treatmentType: 'filling', teeth: ['14', '15'] as FdiToothId[], notes: 'Composite restoration.', }, @@ -157,7 +159,7 @@ const MOCK_HISTORY: Record = { title: 'Hygiene visit', treatmentAt: addCalendarDays(new Date(), -21).toISOString(), status: 'completed', - records: [{ id: 'ptr-a', teeth: [], notes: 'Scale & polish.' }], + records: [{ id: 'ptr-a', treatmentType: 'hygiene', teeth: [], notes: 'Scale & polish.' }], documents: [], }, ], diff --git a/frontend/src/types/treatment.ts b/frontend/src/types/treatment.ts index 4163cc6..79332e3 100644 --- a/frontend/src/types/treatment.ts +++ b/frontend/src/types/treatment.ts @@ -51,8 +51,19 @@ export interface TreatmentAttachmentMeta { sizeBytes: number; } +export const TREATMENT_TYPES = [ + 'consultation', + 'filling', + 'endo', + 'visit', + 'hygiene', +] as const; + +export type TreatmentType = (typeof TREATMENT_TYPES)[number]; + export interface PastTreatmentRecord { id: string; + treatmentType: TreatmentType; teeth: FdiToothId[]; notes?: string | null; } @@ -75,6 +86,7 @@ export interface LinkedOrganizationOption { export interface TreatmentRecordDraft { clientId: string; + treatmentType: TreatmentType; teeth: FdiToothId[]; comment: string; attachmentMetas: TreatmentAttachmentMeta[];