Files
dyolink/.cursor/skills/treatment-workspace/SKILL.md

11 KiB
Raw Blame History

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)

  1. Day stripAppointmentsStrip.tsx renders DayStripItem[] (appointment | unscheduled) via DayStripCard. Timed appointments show the slot; standalone treatments show i18n “No appointment” and sort after timed cards. Empty unscheduled cards (details.length === 0) show a trash control (DELETE /treatments/:id). Workspace fetches GET /appointments and GET /treatments/day. Strip stays dumb (no draft API). New treatment / Walk-in use shared Button.

  2. Treatment previewTreatmentPreviewCard.tsx (read-only summary; no load button for current draft)

  3. Treatment historyPastTreatmentsPanel.tsx (past saved plans for patient; client-side filters in treatmentHistoryFilters.ts)

  4. Editor — detail chips + Add (TreatmentDetailsEditor chrome); 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 activeDetailId resets the wizard to Teeth, unless pendingEntryStepRef is set to lab first (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 AB + BC = one bridge.
  • Shift+click: inclusive same-arch range → all selected as singles (empty circles); overlapping existing bridges are absorbed as singles too. Midline neighbors (1121, 4131) allowed.
  • Prevent browser selection artifacts (select-none, Shift preventDefault on mousedown).
  • On group change, prune/remap labCase.toothProsthesis via pruneToothProsthesisForGroups.
  • Connected UI label: ConnectedSelectionBadge (shared Badge + 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); require viewerSide: 'LAB' | 'CLINIC'. Used in Cases, Tasks, share focus, and Treatment (DetailLabCaseCommentsSection). Composer: h-9 input + 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 treatmentAt matching 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 + LabShipmentBlockedNotice above dispatch when active detail qualifies (isLabDependentDetailMissingTeeth).
  • handleAddLabCase shows toast with labShipmentBlockedBody.
  • 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: DetailLabCaseCommentsSectionLabCaseCommentsPanel + treatmentsApi comment endpoints (viewerSide="CLINIC").
  • Backend includes tasks: { select: { id, status } } on lab cases; mapDetail exposes taskProgress: { completed, total }.

Live lab rail refresh

Inbox Socket.IO notification.creatednotifyTabBadgesChanged() → 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 → auto-create standalone on the selected day.

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 (TreatmentDetailsEditor showChrome). Same gates as before: only when details.length > 1; disabled when !canEdit, day/workspace disabled, detail sent (isDetailLocked), or uploadBusy. Confirm via confirmRemoveDetail. 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 @Transform empty string → undefined before @IsEmail (see patients DTO).

  • Form validation → inline errors; transient feedback → global useToast() via ToastProvider.

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.