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

8.8 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)

  1. Appointments stripAppointmentsStrip.tsx + ScheduleDayPicker.tsx (Today checkbox) + pickAutoAppointment() in components/shared/treatmentSelection.ts

  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 Shift+click range; connected dots
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)
  • 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.

Tooth selection groups

Helpers: frontend/src/components/treatment/toothSelectionGroups.ts. Persisted as toothSelectionGroups on the detail; lab tasks group by selectionGroupId.

  • Plain click: add/remove single; if tooth is in a connected group → drop the whole span and keep only that tooth as a single (no peel / no connected+single pair for the same span).
  • Shift+click: inclusive same-arch range between prior anchor and target; replaces overlapping groups. 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.

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

Below each detail in the editor when the linked lab case is in progress (not all tasks completed):

  • canCommentOnDetailLabCase(detail) — requires sentAt, labCaseId, and !isLabCaseCompleted(taskProgress).
  • UI: DetailLabCaseCommentsSection → existing LabCaseCommentsPanel + treatmentsApi comment endpoints.
  • Backend includes tasks: { select: { id, status } } on lab cases; mapDetail exposes taskProgress: { completed, total }.

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 (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.