diff --git a/.cursor/rules/treatment-workspace.mdc b/.cursor/rules/treatment-workspace.mdc index 20b55b2..54c8891 100644 --- a/.cursor/rules/treatment-workspace.mdc +++ b/.cursor/rules/treatment-workspace.mdc @@ -17,9 +17,9 @@ alwaysApply: false - **Lab dispatch attention:** `LabDispatchAttentionPanel` — unsent lab-dependent details; quick jump to dispatch. - **History API:** patient-scoped; non-owners filtered by provider on treatment or appointment; org owners see all. - **History filters (client-side):** `PastTreatmentsPanel` — “Not shipped to lab” + single date; helpers in `treatmentHistoryFilters.ts`. -- **Lab shipment block:** lab-dependent detail with no teeth saves but cannot ship — `LabShipmentBlockedNotice`, inline banner, toast on add shipment. +- **Lab shipment block:** lab-dependent detail with no teeth saves but cannot ship — same inline amber banner on Treatment and Lab (`labBlockedBannerClass`); toast on add shipment. Empty plan (`[]`) still shows type-first overlay (not a blank Add-only card). - **Edit gating:** `canEditTreatmentForDay` = permission + appointment + not past day + `live` mode. Sent detail locks that line; Add still OK same day. Upload rejects sent (`TREATMENT_DETAIL_SENT`). -- **Lab search:** `LinkedOrganizationSearchCombobox`; invite lab via `/organizations?action=invite-lab` when permitted. +- **Lab search:** `LinkedOrganizationSearchCombobox`; invite lab via `/organizations?action=invite-lab` when permitted. New dispatch lines start empty (lab + apply-all / per-tooth prosthesis type). Chips = last 3 **sent** labs (`labDispatchDefaults.ts`) — never auto-select. Clear the search box when switching details. - **Scroll:** `scrollWithinMainScrollContainer`; shared `Checkbox` only. Full map: `.cursor/skills/treatment-workspace/SKILL.md` diff --git a/.cursor/skills/treatment-workspace/SKILL.md b/.cursor/skills/treatment-workspace/SKILL.md index 4c7eb56..426b784 100644 --- a/.cursor/skills/treatment-workspace/SKILL.md +++ b/.cursor/skills/treatment-workspace/SKILL.md @@ -41,11 +41,11 @@ Right-column entry is **not** a three-step wizard. Type dropdown + `TreatmentDet | Stage | UI | When | |-------|-----|------| | **Treatment** | Type dropdown + `TreatmentDetailAttachmentsStrip`, `FdiToothChart` (Cases scale), full-width Notes | Always | -| **Lab** | `LabCasesDispatchPanel` | Only when active detail type is lab-dependent. Entering Lab auto-ensures a shipment draft. Last-used lab and prosthesis type are remembered. Comments stay on the dispatch panel. | +| **Lab** | `LabCasesDispatchPanel` | Only when active detail type is lab-dependent. Entering Lab auto-ensures a shipment draft. **No default lab or prosthesis type** on a new detail (including siblings in the same plan). Last **3 sent** labs appear as chips under search — pick is explicit. Comments stay on the dispatch panel. | - Prosthesis uses `WizardStepper` (Treatment → Lab) with Back/Next. Lab dispatch keeps comments. - Detail chips show **type + teeth**, not “Detail N”. Lab-dependent chips use colored sent/unsent text (same size as the label); sent date stays on Lab dispatch. -- Detail type may differ from appointment purpose (purpose only defaults new details). +- Detail type may differ from appointment purpose. Purpose only seeds the **first** line of an empty draft — **Add detail** starts with an empty type. - Switching `activeDetailId` resets to the treatment form, unless `pendingEntryStepRef` is set to `lab` first (lab shipments rail / “Go to dispatch” / load-with-focus). - Live draft is **not** duplicated in the left rail preview; preview is for history browse only. @@ -120,7 +120,7 @@ Helpers: `frontend/src/components/treatment/treatmentHistoryFilters.ts`. 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`). +- Inline amber banner in `TreatmentDetailsEditor` on **both** Treatment and Lab steps when the active detail qualifies (`isLabDependentDetailMissingTeeth`). Do not use a separate notice card. - `handleAddLabCase` shows toast with `labShipmentBlockedBody`. - Dispatch panel only appears when a detail passes `isDetailReadyForLabDispatch` (persisted + lab-dependent + teeth). @@ -155,7 +155,7 @@ On today: in-progress slot first, else nearest start time to `now`. Other days: -`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. +`LinkedOrganizationSearchCombobox` in `LabCasesDispatchPanel` — search-only results (no dropdown). Chips under the search are the last **3 labs this clinic sent a case to** (`rememberRecentLab` after successful send). They are shortcuts, not defaults: a new detail’s lab and prosthesis type (apply-all and per-tooth) stay empty until the user chooses. Switching details clears the search box. No match + org tab access → **Invite a lab** navigates to `/organizations?action=invite-lab`. No org access → show permission message; dispatch stops. diff --git a/AGENTS.md b/AGENTS.md index 8416eaa..3f2a573 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,8 +53,9 @@ frontend/src/ - **Schedule date:** Treatment strip and Appointments page headers use `ScheduleDayPicker` `compact` (date centered in a 3-col header; no “Schedule date” label; Today on the navigator). **Treatment lab rules (quick ref):** -- Lab-dependent details (e.g. prosthesis) **without teeth** can save but **cannot ship** — show `LabShipmentBlockedNotice` + inline banner; toast on dispatch add. -- Detail treatment type need **not** match appointment purpose — purpose only pre-fills new details. +- Lab-dependent details (e.g. prosthesis) **without teeth** can save but **cannot ship** — same inline amber banner (`labShipmentBlockedBody`) on Treatment **and** Lab steps; toast on dispatch add. +- New prosthesis dispatch lines start **empty** (no default lab, no apply-all / per-tooth prosthesis type), even for siblings in the same plan. Recent-lab chips are the last **3 sent** destinations — pick is explicit, never auto-selected. +- Detail treatment type need **not** match appointment purpose — purpose only seeds the **first** line of an empty draft; **Add detail** starts with an empty type. - **History filters** are client-side only (`treatmentHistoryFilters.ts`): “Not shipped to lab” + single date on already-fetched patient history; includes live current draft when filtering. - **Lab shipments rail**: unified list with scope toggle **This patient** vs **All updates** (unread across org for **this clinician's cases only**, includes patient name). Opening a case from the rail jumps to the **Lab** send sheet. - **Unread semantics**: Treatment tab badge = count of unread cases **for the user's own treatment plans** (per-case read cursor) and clears when a case is opened/marked read (not on tab visit). diff --git a/frontend/src/components/treatment/labDispatchDefaults.ts b/frontend/src/components/treatment/labDispatchDefaults.ts index 1dc40b0..6ca710d 100644 --- a/frontend/src/components/treatment/labDispatchDefaults.ts +++ b/frontend/src/components/treatment/labDispatchDefaults.ts @@ -1,73 +1,60 @@ const STORAGE_PREFIX = 'dyolink.labDispatchDefaults.'; -export type LabDispatchDefaults = { - lastLabId: string | null; - lastProsthesisByLab: Record; -}; - -const EMPTY: LabDispatchDefaults = { - lastLabId: null, - lastProsthesisByLab: {}, -}; +/** Chips under lab search — last destinations this clinic actually sent a case to. */ +export const MAX_RECENT_LABS = 3; function storageKey(clinicOrganizationId: string): string { return `${STORAGE_PREFIX}${clinicOrganizationId}`; } -export function loadLabDispatchDefaults(clinicOrganizationId: string | null | undefined): LabDispatchDefaults { - if (!clinicOrganizationId || typeof window === 'undefined') return EMPTY; +function uniqueIds(ids: string[]): string[] { + const out: string[] = []; + for (const id of ids) { + if (id && !out.includes(id)) out.push(id); + } + return out; +} + +export function loadRecentLabIds(clinicOrganizationId: string | null | undefined): string[] { + if (!clinicOrganizationId || typeof window === 'undefined') return []; try { const raw = window.localStorage.getItem(storageKey(clinicOrganizationId)); - if (!raw) return EMPTY; - const parsed = JSON.parse(raw) as Partial; - return { - lastLabId: typeof parsed.lastLabId === 'string' ? parsed.lastLabId : null, - lastProsthesisByLab: - parsed.lastProsthesisByLab && typeof parsed.lastProsthesisByLab === 'object' - ? parsed.lastProsthesisByLab - : {}, + if (!raw) return []; + const parsed = JSON.parse(raw) as { + recentLabIds?: unknown; + lastLabId?: unknown; }; + const fromList = Array.isArray(parsed.recentLabIds) + ? parsed.recentLabIds.filter((id): id is string => typeof id === 'string') + : []; + // Older builds stored a single lastLabId used as an auto-selected default. + const fromLegacy = typeof parsed.lastLabId === 'string' ? [parsed.lastLabId] : []; + return uniqueIds([...fromList, ...fromLegacy]).slice(0, MAX_RECENT_LABS); } catch { - return EMPTY; + return []; } } -function writeDefaults(clinicOrganizationId: string, next: LabDispatchDefaults): void { +function writeRecentLabIds(clinicOrganizationId: string, recentLabIds: string[]): void { if (typeof window === 'undefined') return; try { - window.localStorage.setItem(storageKey(clinicOrganizationId), JSON.stringify(next)); + window.localStorage.setItem( + storageKey(clinicOrganizationId), + JSON.stringify({ recentLabIds }), + ); } catch { // Ignore quota / private-mode failures. } } -export function rememberLastLab(clinicOrganizationId: string | null | undefined, labId: string): void { - if (!clinicOrganizationId || !labId) return; - const current = loadLabDispatchDefaults(clinicOrganizationId); - writeDefaults(clinicOrganizationId, { ...current, lastLabId: labId }); -} - -export function rememberLastProsthesisType( +/** Record a lab after a successful send — suggestion chips only, never a form default. */ +export function rememberRecentLab( clinicOrganizationId: string | null | undefined, labId: string, - prosthesisTypeCode: string, ): void { - if (!clinicOrganizationId || !labId || !prosthesisTypeCode) return; - const current = loadLabDispatchDefaults(clinicOrganizationId); - writeDefaults(clinicOrganizationId, { - ...current, - lastLabId: labId, - lastProsthesisByLab: { - ...current.lastProsthesisByLab, - [labId]: prosthesisTypeCode, - }, - }); -} - -export function lastProsthesisTypeForLab( - clinicOrganizationId: string | null | undefined, - labId: string | null | undefined, -): string | null { - if (!clinicOrganizationId || !labId) return null; - return loadLabDispatchDefaults(clinicOrganizationId).lastProsthesisByLab[labId] ?? null; + if (!clinicOrganizationId || !labId) return; + writeRecentLabIds( + clinicOrganizationId, + uniqueIds([labId, ...loadRecentLabIds(clinicOrganizationId)]).slice(0, MAX_RECENT_LABS), + ); } diff --git a/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx b/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx index 6b8bc7a..68860d6 100644 --- a/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx +++ b/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/shared/Button'; import { Checkbox } from '@/components/ui/shared/Checkbox'; @@ -16,12 +16,6 @@ import { LabCaseTrackerCard } from '@/components/ui/treatment/LabCaseTrackerCard import { treatmentTypeLabelFromCatalog } from '@/components/shared/treatmentTypeDisplay'; import { treatmentsApi } from '@/lib/api/treatments'; import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog'; -import { - lastProsthesisTypeForLab, - loadLabDispatchDefaults, - rememberLastLab, - rememberLastProsthesisType, -} from '@/components/treatment/labDispatchDefaults'; import type { ProsthesisCatalogEntry, TreatmentCatalogEntry } from '@/types/treatment-catalog'; import type { LabCaseDraft, LinkedOrganizationOption, TreatmentDetailDraft } from '@/types/treatment'; import type { PatientLabCaseSummary } from '@/types/lab-case-activity'; @@ -34,7 +28,6 @@ interface LabCasesDispatchPanelProps { labCases: LabCaseDraft[]; labDependentCodes: Set; treatmentCatalog: TreatmentCatalogEntry[]; - clinicOrganizationId?: string | null; labCaseSummary?: PatientLabCaseSummary | null; locale: string; onLabCaseSummaryChange?: (summary: PatientLabCaseSummary) => void; @@ -48,7 +41,6 @@ interface LabCasesDispatchPanelProps { organizationSearch: string; onOrganizationSearchChange: (value: string) => void; recentOrganizationIds: string[]; - onRecentOrganizationPick: (orgId: string) => void; canInviteLab?: boolean; onInviteLab?: () => void; sendBusyId: string | null; @@ -124,7 +116,6 @@ export function LabCasesDispatchPanel({ labCases, labDependentCodes, treatmentCatalog, - clinicOrganizationId, labCaseSummary, locale, onLabCaseSummaryChange, @@ -138,7 +129,6 @@ export function LabCasesDispatchPanel({ organizationSearch, onOrganizationSearchChange, recentOrganizationIds, - onRecentOrganizationPick, canInviteLab = false, onInviteLab, sendBusyId, @@ -150,7 +140,6 @@ export function LabCasesDispatchPanel({ const [prosthesisOptions, setProsthesisOptions] = useState([]); const [applyAllProsthesis, setApplyAllProsthesis] = useState(''); const [pendingComment, setPendingComment] = useState(''); - const autoFilledCaseRef = useRef(null); const hasTrackerSummary = Boolean(labCaseSummary && labCaseSummary.labCaseId); const activeLinkedOrganizations = orgs.filter((o) => o.active); @@ -168,7 +157,14 @@ export function LabCasesDispatchPanel({ const activeLabCase = labCaseForActiveDetail ?? - (activeLabCaseId ? labCases.find((lc) => lc.clientId === activeLabCaseId) : null); + (activeLabCaseId + ? labCases.find( + (lc) => + lc.clientId === activeLabCaseId && + (lc.detailClientId == null || lc.detailClientId === activeDetailId), + ) + : null) ?? + null; const sent = Boolean(activeLabCase?.sentAt); const activeDetailNumber = details.findIndex((d) => d.clientId === activeDetailId) + 1; @@ -208,6 +204,7 @@ export function LabCasesDispatchPanel({ useEffect(() => { setPendingComment(''); + setApplyAllProsthesis(''); }, [activeLabCase?.clientId]); function updateActiveLabCase(patch: Partial) { @@ -226,45 +223,6 @@ export function LabCasesDispatchPanel({ // eslint-disable-next-line react-hooks/exhaustive-deps -- only sync newly uploaded files }, [activeDetail?.attachmentMetas, activeLabCase?.clientId, sent]); - useEffect(() => { - if (!activeLabCase || sent || activeLabCase.destinationOrganizationId) return; - const lastLabId = loadLabDispatchDefaults(clinicOrganizationId).lastLabId; - const lastLab = lastLabId - ? activeLinkedOrganizations.find((o) => o.id === lastLabId) - : undefined; - if (!lastLab) return; - updateActiveLabCase({ destinationOrganizationId: lastLab.id, toothProsthesis: [] }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeLabCase?.clientId, clinicOrganizationId, sent]); - - useEffect(() => { - if (!activeLabCase || sent) return; - if (!activeLabCase.destinationOrganizationId) return; - if (prosthesisOptions.length === 0 || prosthesisRows.length === 0) return; - const fillKey = `${activeLabCase.clientId}:${prosthesisRows.length}`; - if (autoFilledCaseRef.current === fillKey) return; - if (isProsthesisMapComplete(activeLabCase, prosthesisRows)) { - autoFilledCaseRef.current = fillKey; - return; - } - const lastCode = lastProsthesisTypeForLab( - clinicOrganizationId, - activeLabCase.destinationOrganizationId, - ); - if (!lastCode || !prosthesisOptions.some((opt) => opt.code === lastCode)) return; - autoFilledCaseRef.current = fillKey; - setApplyAllProsthesis(lastCode); - updateActiveLabCase({ toothProsthesis: toothProsthesisForRows(prosthesisRows, lastCode) }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - activeLabCase?.clientId, - activeLabCase?.destinationOrganizationId, - clinicOrganizationId, - prosthesisOptions, - prosthesisRows.length, - sent, - ]); - if (!activeDetail || !isLabDependentDetail) { return null; } @@ -287,25 +245,11 @@ export function LabCasesDispatchPanel({ ] : rest; updateActiveLabCase({ toothProsthesis: next }); - if (prosthesisTypeCode && activeLabCase.destinationOrganizationId) { - rememberLastProsthesisType( - clinicOrganizationId, - activeLabCase.destinationOrganizationId, - prosthesisTypeCode, - ); - } } function applyProsthesisToAll(code: string) { if (!activeLabCase || !code) return; updateActiveLabCase({ toothProsthesis: toothProsthesisForRows(prosthesisRows, code) }); - if (activeLabCase.destinationOrganizationId) { - rememberLastProsthesisType( - clinicOrganizationId, - activeLabCase.destinationOrganizationId, - code, - ); - } } function toggleAttachmentInActiveLabCase(attachmentId: string, checked: boolean) { @@ -317,13 +261,11 @@ export function LabCasesDispatchPanel({ } function handleSelectOrganization(org: LinkedOrganizationOption) { - autoFilledCaseRef.current = null; updateActiveLabCase({ destinationOrganizationId: org.id, toothProsthesis: [], }); setApplyAllProsthesis(''); - rememberLastLab(clinicOrganizationId, org.id); } const caseFullyComplete = isLabCaseCompleted(activeLabCase?.taskProgress); @@ -463,10 +405,7 @@ export function LabCasesDispatchPanel({ key={o.id} type="button" disabled={disabled} - onClick={() => { - handleSelectOrganization(o); - onRecentOrganizationPick(o.id); - }} + onClick={() => handleSelectOrganization(o)} className="text-xs rounded-[var(--radius-sm)] border border-border/70 px-2 py-1 text-text-secondary hover:text-text-primary hover:border-border focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 disabled:opacity-50" > {o.name} @@ -615,26 +554,7 @@ export function LabCasesDispatchPanel({ !prosthesisComplete } isLoading={sendBusyId === activeLabCase.clientId} - onClick={() => { - if (activeLabCase.destinationOrganizationId) { - rememberLastLab(clinicOrganizationId, activeLabCase.destinationOrganizationId); - const codes = [ - ...new Set( - activeLabCase.toothProsthesis - .map((tp) => tp.prosthesisTypeCode) - .filter(Boolean), - ), - ]; - if (codes.length === 1) { - rememberLastProsthesisType( - clinicOrganizationId, - activeLabCase.destinationOrganizationId, - codes[0], - ); - } - } - return onSendLabCase(activeLabCase, pendingComment.trim()); - }} + onClick={() => onSendLabCase(activeLabCase, pendingComment.trim())} > {t('sendToLab')} diff --git a/frontend/src/components/ui/treatment/LabShipmentBlockedNotice.tsx b/frontend/src/components/ui/treatment/LabShipmentBlockedNotice.tsx deleted file mode 100644 index 71eddee..0000000 --- a/frontend/src/components/ui/treatment/LabShipmentBlockedNotice.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import { AlertCircle } from 'lucide-react'; -import { useTranslations } from 'next-intl'; - -export function LabShipmentBlockedNotice() { - const t = useTranslations('treatment'); - - return ( -
-
- -
-

{t('labShipmentBlockedTitle')}

-

{t('labShipmentBlockedBody')}

-
-
-
- ); -} diff --git a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx index 884ffdc..dd7cedd 100644 --- a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx +++ b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx @@ -28,7 +28,7 @@ interface TreatmentDetailsEditorProps { canEdit: boolean; saveStatus: 'idle' | 'dirty' | 'saving' | 'saved' | 'error'; uploadBusy: boolean; - onAddDetail: () => void; + onAddDetail: (initialType?: string) => void; onRemoveDetail?: (detailClientId: string) => void; onUploadFiles: (files: File[], onProgress: (percent: number) => void) => Promise; onRemoveAttachment?: (attachmentId: string) => void; @@ -113,7 +113,7 @@ export function TreatmentDetailsEditor({ type="button" variant="primary" disabled={!canEdit || disabled} - onClick={onAddDetail} + onClick={() => onAddDetail()} fullWidth className="sm:w-auto shrink-0" > @@ -203,12 +203,12 @@ export function TreatmentDetailsEditor({ {stepper && activeDetail ?
{stepper}
: null} + {activeDetail && showMissingTeethLabBlock ? ( +

{t('labShipmentBlockedBody')}

+ ) : null} + {showFields && activeDetail ? (
- {showMissingTeethLabBlock && ( -

{t('labShipmentBlockedBody')}

- )} -
{footer}
: null}
+ ) : showFields && !activeDetail ? ( +
+ { + const nextType = e.target.value; + if (nextType) onAddDetail(nextType); + }} + disabled={!canEdit || disabled} + > + + {treatmentCatalog.map((entry, index) => ( + + ))} + + {chart ? ( +
+
+

{chartLockMessage}

+
+
{chart}
+
+ ) : chartLockMessage ? ( +

{chartLockMessage}

+ ) : null} +
) : null} ); diff --git a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx index 4d60d18..26c9517 100644 --- a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx +++ b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx @@ -15,7 +15,6 @@ import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip'; import { NewTreatmentPatientPicker } from '@/components/ui/treatment/NewTreatmentPatientPicker'; import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart'; import { LabCasesDispatchPanel } from '@/components/ui/treatment/LabCasesDispatchPanel'; -import { LabShipmentBlockedNotice } from '@/components/ui/treatment/LabShipmentBlockedNotice'; import { LabDispatchAttentionPanel } from '@/components/ui/treatment/LabDispatchAttentionPanel'; import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel'; import { TreatmentDetailsEditor } from '@/components/ui/treatment/TreatmentDetailsEditor'; @@ -57,7 +56,11 @@ import { } from '@/components/treatment/toothSelectionGroups'; import type { LabDispatchAttentionItem } from '@/components/treatment/labDispatchAttention'; import { collectLabDispatchAttention } from '@/components/treatment/labDispatchAttention'; -import { loadLabDispatchDefaults, rememberLastLab } from '@/components/treatment/labDispatchDefaults'; +import { + loadRecentLabIds, + MAX_RECENT_LABS, + rememberRecentLab, +} from '@/components/treatment/labDispatchDefaults'; import { canEditTreatment, canViewTreatment, canAccessDashboardRoute } from '@/components/shared/permissions'; import { scrollWithinMainScrollContainer } from '@/components/shared/scrollWithinMain'; import { useMarkTabReadOnVisit, useTabBadgeCounts } from '@/lib/hooks/useTabBadgeCounts'; @@ -735,7 +738,7 @@ export function TreatmentWorkspace({ mapped.length > 0 ? mapped : options?.seedBlankIfEmpty - ? [newDetail(defaultTreatmentTypeForAppointment(undefined, treatmentCatalog))] + ? [newDetail()] : []; setDetails(nextDetails); setActiveDetailId((prev) => { @@ -750,7 +753,7 @@ export function TreatmentWorkspace({ setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null); setOrganizationSearch(''); setSaveStatus('idle'); - }, [treatmentCatalog]); + }, []); const selectedTeethSet = useMemo(() => new Set(activeDetail?.teeth ?? []), [activeDetail?.teeth]); const connectedSelectedTeeth = useMemo( @@ -803,6 +806,7 @@ export function TreatmentWorkspace({ useEffect(() => { setShowWholeTreatmentPlan(false); rangeAnchorRef.current = null; + setOrganizationSearch(''); const pending = pendingEntryStepRef.current; pendingEntryStepRef.current = null; setEntryStep(pending ?? 'treatment'); @@ -910,11 +914,11 @@ export function TreatmentWorkspace({ setLabDependentCodes( new Set(catalogResponse.data.filter((entry) => entry.labDependent).map((entry) => entry.code)), ); - const lastLabId = loadLabDispatchDefaults(currentOrganization?.id).lastLabId; - if (lastLabId && orgsResponse.data.some((o) => o.id === lastLabId && o.active)) { - setRecentOrganizationIds((prev) => - prev.includes(lastLabId) ? prev : [lastLabId, ...prev].slice(0, 10), - ); + const recentIds = loadRecentLabIds(currentOrganization?.id).filter((id) => + orgsResponse.data.some((o) => o.id === id && o.active), + ); + if (recentIds.length > 0) { + setRecentOrganizationIds(recentIds); } } catch (error: unknown) { if (!cancelled) { @@ -1959,14 +1963,9 @@ export function TreatmentWorkspace({ return; } - const lastLabId = loadLabDispatchDefaults(currentOrganization?.id).lastLabId; - const lastLabStillActive = lastLabId - ? orgs.some((o) => o.id === lastLabId && o.active) - : false; const next: LabCaseDraft = { ...newLabCaseDraft(), detailClientId: shouldIncludeActive ? activeDetailId : null, - destinationOrganizationId: lastLabStillActive ? lastLabId! : null, attachmentIds: activeDetail?.attachmentMetas.map((a) => a.id) ?? [], }; const updatedLabCases = [...cleaned, next]; @@ -1982,11 +1981,9 @@ export function TreatmentWorkspace({ }, [ activeDetailId, canEditTreatmentForDay, - currentOrganization?.id, details, labCaseDrafts, labDependentCodes, - orgs, persistDraft, persistLabCases, selectedAppointment, @@ -2106,8 +2103,8 @@ export function TreatmentWorkspace({ setRecentOrganizationIds((prev) => { const orgId = labCase.destinationOrganizationId!; - rememberLastLab(currentOrganization?.id, orgId); - return [orgId, ...prev.filter((id) => id !== orgId)].slice(0, 10); + rememberRecentLab(currentOrganization?.id, orgId); + return [orgId, ...prev.filter((id) => id !== orgId)].slice(0, MAX_RECENT_LABS); }); showSuccess(t('successCaseSent')); notifyTabBadgesChanged(); @@ -2345,10 +2342,8 @@ export function TreatmentWorkspace({ canEdit={canEdit} saveStatus={saveStatus} uploadBusy={uploadBusyDetailId === activeDetailId} - onAddDetail={() => { - const next = newDetail( - defaultTreatmentTypeForAppointment(selectedAppointment?.purpose, treatmentCatalog), - ); + onAddDetail={(initialType) => { + const next = newDetail(initialType); setDetails((prev) => [...prev, next]); setActiveDetailId(next.clientId); setEntryStep('treatment'); @@ -2587,7 +2582,6 @@ export function TreatmentWorkspace({ {entryStep === 'lab' ? (
- {showLabShipmentBlocked ? : null} {showLabDispatchPanel ? ( { - setLabCaseDrafts((prev) => { - const targetId = - activeLabCaseId ?? - prev.find((lc) => !lc.sentAt && lc.detailClientId === activeDetailId) - ?.clientId; - if (!targetId) return prev; - return prev.map((lc) => - lc.clientId === targetId && !lc.sentAt - ? { ...lc, destinationOrganizationId: orgId } - : lc, - ); - }); - }} sendBusyId={sendBusyId} onSendLabCase={(lc, comment) => handleSendLabCase(lc, comment)} onCommentError={showError} canInviteLab={canAccessOrganizations} onInviteLab={() => router.push('/organizations?action=invite-lab')} /> - ) : ( + ) : showLabShipmentBlocked ? null : (

{t('entryStepLabUnavailable')}