12 KiB
name, description
| name | description |
|---|---|
| dyolink-treatment-workspace | Treatment tab workspace — appointments strip, preview vs form, history, load flow, draft autosave, entry wizard, connected teeth, lab dispatch. Use when changing treatment UX, preview/history, or lab dispatch in TreatmentWorkspace. |
Treatment workspace
Main orchestrator: frontend/src/components/ui/treatment/TreatmentWorkspace.tsx
Thin route: app/[locale]/(dashboard)/treatment/page.tsx (supports ?appointmentId=).
Layout (top → bottom)
-
Day strip —
AppointmentsStrip.tsxrendersDayStripItem[](appointment|unscheduled) viaDayStripCard. Timed appointments keep treatment-type pastel banners. Standalone cards keep the same size/content but use detail-chip theming (neutral border, selectedborder-primary bg-primary-soft, trash with chip delete hover). Empty unscheduled cards (details.length === 0) can be deleted (DELETE /treatments/:id). Workspace fetchesGET /appointmentsandGET /treatments/day. New treatment is one sharedButton: it opensNewTreatmentPatientPicker(Walk-in always first, then search). Creating happens only after an explicit patient choice — never from the selected appointment card. -
Treatment preview —
TreatmentPreviewCard.tsx(read-only summary; no load button for current draft) -
Treatment history —
PastTreatmentsPanel.tsx(past saved plans for patient; client-side filters intreatmentHistoryFilters.ts) -
Editor — detail chips + Add (
TreatmentDetailsEditorchrome); entry wizard (WizardStepper); step panels:FdiToothChart, Content fields,LabCasesDispatchPanel
Entry wizard (Teeth / Content / Lab)
Right-column entry uses WizardStepper (components/ui/shared/WizardStepper.tsx) — numbered nodes + connector rail. Do not reuse detail-chip tab styling for steps.
| Step | UI | Notes |
|---|---|---|
| Teeth | FdiToothChart |
Neighbor circles connect/disconnect; Shift+range selects (empty circles); plain click select/deselect |
| Content | TreatmentDetailsEditor fields only (showChrome={false}) |
Type / Notes / attachments — no delete button |
| Lab | LabCasesDispatchPanel |
Shown in stepper only when active detail type is lab-dependent (labDependentCodes). Entering Lab auto-ensures a shipment draft (no “Add lab shipment” click). Add detail stays ungated. Due date beside title (RTL via logical end). Prosthesis dropdown same-row from md. |
- Detail type may differ from appointment purpose (purpose only defaults new details).
- Next/Back navigate visible steps; leaving prosthesis while on Lab returns to Content.
- Switching
activeDetailIdresets the wizard to Teeth, unlesspendingEntryStepRefis set tolabfirst (lab shipments rail / “Go to dispatch” / load-with-focus). That ref prevents the reset effect from overwriting Lab on the first click.
Tooth selection groups
Helpers: frontend/src/components/treatment/toothSelectionGroups.ts. Persisted as toothSelectionGroups on the detail. Lab dispatch rows are 1:1 with selection groups; after send, Cases/Tasks merge teeth by prosthesis type (not by selection group).
- Plain click: add/remove single; deselecting a tooth in a bridge removes it and splits/shrinks remaining sides (never a 1-tooth connected group).
- Neighbor circles: when two arch-adjacent teeth are both selected, an empty circle appears between them (not per-tooth). Click empty → connect; click filled → disconnect (teeth stay selected). Transitive A–B + B–C = one bridge.
- Shift+click: inclusive same-arch range → all selected as singles (empty circles); overlapping existing bridges are absorbed as singles too. Midline neighbors (11–21, 41–31) allowed.
- Prevent browser selection artifacts (
select-none, ShiftpreventDefaulton mousedown). - On group change, prune/remap
labCase.toothProsthesisviapruneToothProsthesisForGroups. - Connected UI label:
ConnectedSelectionBadge(sharedBadge+ primary tint) in dispatch + lab case lists. - Lab case comments: shared
LabCaseCommentsPanel— newest-first; sent =justify-start/text-start, received =justify-end/text-end(RTL-safe); requireviewerSide: 'LAB' | 'CLINIC'. Used in Cases, Tasks, share focus, and Treatment (DetailLabCaseCommentsSection). Composer:h-9input + primary send / visibility buttons (white icons; Send mirrored in RTL).
Two layers of state (critical)
| Layer | State | Updated when |
|-------|--------|--------------|
| Preview | previewTreatment; selectedPreviewId !== null = browse mode | History click updates preview only |
| Form | details[], labCaseDrafts[] | Appointment change → draft API; Load into workspace → hydrate |
Browse mode: banner + “Load into workspace” / “Back to current draft”. No Open button on preview card.
Lab dispatch attention: LabDispatchAttentionPanel lists lab-dependent unsent details (current draft + user history). “Go to dispatch” / “Load & dispatch”.
History API
GET /treatments/patients/:id/history returns saved treatments for that patient scoped to the logged-in clinician (Treatment.providerUserId or linked Appointment.providerUserId). Owners are not exempt — each user only sees plans they created or own via their appointments.
History filters (client-side only)
PastTreatmentsPanel filters already-fetched history — no extra API params.
- Not shipped to lab — show treatments that have at least one lab-dependent detail (prosthesis via
labDependentCodes) with!sentAt. - Date — filter on
treatmentAtmatching that local calendar day. - When not shipped is on, filters already-fetched history only (excludes the active appointment row).
- Previous treatments rail excludes the active appointment; live draft stays in Current draft preview only. Refresh history after lab send.
Helpers: frontend/src/components/treatment/treatmentHistoryFilters.ts.
Lab shipment without teeth
Saved lab-dependent detail with no teeth can autosave but cannot create a lab shipment.
- Inline banner in
TreatmentDetailsEditor+LabShipmentBlockedNoticeabove dispatch when active detail qualifies (isLabDependentDetailMissingTeeth). handleAddLabCaseshows toast withlabShipmentBlockedBody.- Dispatch panel only appears when a detail passes
isDetailReadyForLabDispatch(persisted + lab-dependent + teeth).
Lab case comments on details
Comments for a shipment live in the Lab dispatch panel (and Cases/Tasks/share), not on Content notes.
- Unsent: deferred composer in
DetailLabCaseCommentsSection(posts with Send to lab). - Sent / in progress: live composer until tasks complete (
canPostComments). - UI:
DetailLabCaseCommentsSection→LabCaseCommentsPanel+treatmentsApicomment endpoints (viewerSide="CLINIC"). - Backend includes
tasks: { select: { id, status } }on lab cases;mapDetailexposestaskProgress: { completed, total }.
Live lab rail refresh
Inbox Socket.IO notification.created → notifyTabBadgesChanged() → silent refresh of patient lab cases + unread rail. Does not remount the workspace or clear draft/form state. See .cursor/skills/notifications-inbox/SKILL.md.
Appointments default selection
On today: in-progress slot first, else nearest start time to now. Other days: first appointment. Re-runs every 60s on today unless selectionLocked. Frontend filters appointments to providerUserId === userId.
Today checkbox (ScheduleDayPicker): unchecked when selectedDay is not today (e.g. after loading a historical treatment). Checking Today calls onSelectDay(today) which unlocks selection (selectionLocked = false) and resets browse/historical context; appointments reload and auto-select nearest to now.
Lab org search (dispatch)
LinkedOrganizationSearchCombobox in LabCasesDispatchPanel — search-only results (no dropdown). No match + org tab access → Invite a lab navigates to /organizations?action=invite-lab. No org access → show permission message; dispatch stops.
Pattern mirrors PatientSearchCombobox in appointments.
Scroll
Use scrollWithinMainScrollContainer() (not raw scrollIntoView) when jumping to lab dispatch panel — dashboard <main> is the scroll container; document scroll conflicts with .app-web-bg { overflow: hidden }.
Use shared Checkbox (not native <input type="checkbox">) to avoid focus-driven scroll jumps.
Backend APIs
| Endpoint | Purpose |
|----------|---------|
| GET /appointments?from&to | Timed strip cards |
| GET /treatments/day?from&to | Standalone (unscheduled) strip cards |
| POST /treatments | Create standalone { patientId?, walkIn?, treatmentAt } |
| DELETE /treatments/:id | Empty standalone only (appointmentId null, no details) |
| GET /treatments/patients/:patientId/history | History (patient + org; filtered by provider) |
| GET /treatments/appointments/:id/draft | Load form on appointment select |
| GET/PUT /treatments/:treatmentId/draft | Load/save when there is no appointment |
| PUT .../lab-cases | Autosave (600ms debounce) — appointment or treatment id |
Walk-in uses one sentinel Patient per clinic (isWalkIn, hidden from Patients/search/booking). Display via i18n, never the stored name. Patient search: same workspace patient → no-op; else load latest history into the editor; no history → do not auto-create (history rail empties; dentist uses New treatment and picks a patient, including Walk-in).
Draft writes for appointments require provider match (ensureAppointmentProvider). Standalone requires treatment.providerUserId === actor.
Edit gating
canEditTreatmentForDay = canEdit && hasLiveContext && !isViewingPastDay && workspaceMode === 'live'
hasLiveContext is a selected live appointment or standalone treatment. Past day or historical workspace mode freezes the treatment form + most lab-dispatch fields.
Per-detail sent lock
isDetailLocked = linked lab case has sentAt. Locked details: type, comment, teeth, attachments are read-only (UI + draft save skips updates/deletes; attachment upload returns TREATMENT_DETAIL_SENT).
Add / remove details
- Add detail stays enabled whenever
canEditTreatmentForDay— even if sibling details are already sent. - Remove detail: trash icon on each detail chip in chrome (
TreatmentDetailsEditorshowChrome). Same gates as before: only whendetails.length > 1; disabled when!canEdit, day/workspacedisabled, detail sent (isDetailLocked), oruploadBusy. Confirm viaconfirmRemoveDetail. Drop linked unsent lab drafts with the detail. Do not put a delete control in the Content wizard step.
Sent lab shipment fields (vs detail)
After send, destination / prosthesis / shipment attachments are frozen. Due date stays editable until all lab tasks complete (UI still respects day/mode disabled). Comments stay editable until tasks complete and intentionally ignore the day/mode gate.
Full map helpers: treatmentDetailRules.ts, LabCasesDispatchPanel.tsx.
-
@IsOptional()email: use@Transformempty string →undefinedbefore@IsEmail(see patients DTO). -
Form validation → inline errors; transient feedback → global
useToast()viaToastProvider.
When changing history scope
Filter in backend listPatientHistory / lab-case lists on patient + org + provider scope (common/treatment-provider-scope.ts). History is per selected patient and per clinician, not per day or all org plans.
UI filters (not shipped, date) are client-side only — do not add API params unless product explicitly requires server-side filtering.