10 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)
-
Appointments strip —
AppointmentsStrip.tsx+ScheduleDayPicker.tsx(Today checkbox) +pickAutoAppointment()incomponents/shared/treatmentSelection.ts -
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 |
Shift+click connected bridge (union if overlap); click cuts tooth from bridge as selected single |
| 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; if tooth is in a connected bridge → cut it out as a selected single (second click deselects). End of bridge → remainder stays connected (≥2) or demotes to single. Middle → left and right become separate groups. Never a 1-tooth connected (no lone dots).
- Shift+click: inclusive same-arch range → one connected bridge; if the range overlaps existing bridges, union them into one bridge. Non-overlapping bridges stay separate.
- 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 | Strip |
| GET /treatments/patients/:patientId/history | History (patient + org; filtered by provider) |
| GET /treatments/appointments/:id/draft | Load form on appointment select |
| PUT .../draft, PUT .../lab-cases | Autosave (600ms debounce) |
Draft writes require provider match (ensureAppointmentProvider) unless org owner.
Edit gating
canEditTreatmentForDay = canEdit && selectedAppointment && !isViewingPastDay && workspaceMode === 'live'
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.