---

name: dyolink-treatment-workspace

description: 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 strip** — `AppointmentsStrip.tsx` + `ScheduleDayPicker.tsx` (Today checkbox) + `pickAutoAppointment()` in `components/shared/treatmentSelection.ts`

2. **Treatment preview** — `TreatmentPreviewCard.tsx` (read-only summary; no load button for current draft)

3. **Treatment history** — `PastTreatmentsPanel.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 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`, 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: `DetailLabCaseCommentsSection` → `LabCaseCommentsPanel` + `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.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

```typescript
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.

