From 7f02a84ebfae4727bea57103f06d8e3f62c309ca Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 22 Aug 2026 20:25:11 +0330 Subject: [PATCH] improvement: a flow added to create treatment plan for searched patients with no treatment history. --- .cursor/rules/treatment-workspace.mdc | 2 +- .cursor/skills/treatment-workspace/SKILL.md | 2 +- AGENTS.md | 2 +- frontend/messages/en.json | 2 + frontend/messages/fa.json | 2 + frontend/messages/nl.json | 2 + .../ui/treatment/TreatmentWorkspace.tsx | 86 +++++++++++++++---- 7 files changed, 80 insertions(+), 18 deletions(-) diff --git a/.cursor/rules/treatment-workspace.mdc b/.cursor/rules/treatment-workspace.mdc index e4c4412..dbc5ecd 100644 --- a/.cursor/rules/treatment-workspace.mdc +++ b/.cursor/rules/treatment-workspace.mdc @@ -14,7 +14,7 @@ alwaysApply: false - **Lab dispatch UI:** due date end-aligned beside title (`sm:flex-row` + `justify-between`; stacks on mobile). Prosthesis type: stacked below `md`, 50/50 same-row from `md`. Content clinical field = **Notes** (not case comments). - **Lab comments:** shared `LabCaseCommentsPanel` (newest-first; sent=`justify-start`, received=`justify-end`; `viewerSide`) across Treatment / Cases / Tasks / share. Use logical `text-start`/`text-end`, not left/right. - **Detail chrome:** chips (type + teeth) + Add at top; **Remove** = trash on chip (unsent, including last line; disabled when day-locked / no edit / uploading). Last-line confirm: plan will be empty until Add. Empty `[]` shows `noDetails` (not the type-first overlay). New treatment seeds one blank detail. No delete in type/notes fields. -- **Patient search:** `PatientSearchCombobox` in the **page header** (workspace-wide). **New treatment** stays at the top of the left rail; selected-patient card sits under it. Picker: Walk-in first, then a matching full-width card for the current named patient (name + mobile/email, else hint), then search. Never auto-create from the appointment card. +- **Patient search:** `PatientSearchCombobox` in the **page header** (workspace-wide). Opens today’s strip visit if any, else latest history. No visit/history → inline editor empty state pointing to **New treatment** in the rail (never a dialog, never auto-create). **New treatment** stays at the top of the left rail; selected-patient card sits under it. Picker: Walk-in first, then a matching full-width card for the current named patient (name + mobile/email, else hint), then search. Never auto-create from the appointment card. - **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`. diff --git a/.cursor/skills/treatment-workspace/SKILL.md b/.cursor/skills/treatment-workspace/SKILL.md index 55e8974..932ab50 100644 --- a/.cursor/skills/treatment-workspace/SKILL.md +++ b/.cursor/skills/treatment-workspace/SKILL.md @@ -201,7 +201,7 @@ Use shared `Checkbox` (not native ``) to avoid focus-driv -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 → do not auto-create** (history rail empties; dentist uses **New treatment** and picks Walk-in, the current named patient card, or search). +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 with a live visit → no-op; else open today’s strip visit if any; else load latest history into the editor; **no history and no strip visit → do not auto-create**. Detach the previous visit, keep the searched patient, and show an inline editor empty state (`noTreatmentFoundTitle` / `noTreatmentFoundBody`) that points to **New treatment** in the rail (Walk-in, current named patient card, or search). Draft writes for appointments require provider match (`ensureAppointmentProvider`). Standalone requires `treatment.providerUserId === actor`. diff --git a/AGENTS.md b/AGENTS.md index 7ab5676..4cb937a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ frontend/src/ **Treatment tab:** Preview and editable form are **separate** until the user clicks **Load into workspace** on a history item. See `.cursor/skills/treatment-workspace/SKILL.md` before changing that flow. **Treatment edit / details (quick ref):** -- Day/mode gate: editable only for live draft on today/future (`canEditTreatmentForDay`). Past day / historical load → read-only form. Patient search sits in the **page header** (workspace-wide). **New treatment** is at the top of the left rail (selected-patient card below it) and opens a picker: Walk-in first, then a matching card for the current named patient (name + mobile/email), then search. It does not auto-copy the appointment card’s patient. New treatment seeds one blank detail; a persisted empty plan loads as `[]` until Add. +- Day/mode gate: editable only for live draft on today/future (`canEditTreatmentForDay`). Past day / historical load → read-only form. Patient search sits in the **page header** (workspace-wide). If the patient has a visit on the day strip, that visit opens; else the latest history plan loads. If neither exists, the editor shows an inline empty state (no dialog) pointing to **New treatment** in the rail — do not auto-create. **New treatment** is at the top of the left rail (selected-patient card below it) and opens a picker: Walk-in first, then a matching card for the current named patient (name + mobile/email), then search. It does not auto-copy the appointment card’s patient. New treatment seeds one blank detail; a persisted empty plan loads as `[]` until Add. - **Unscheduled strip cards** use the same treatment-type banner as appointments from the **first detail’s type only** (`unscheduledStripColorCode`; live draft for the open card). Empty first line → chip theming even if later lines are typed. Strip trash only when every line is blank (no type/teeth/notes/attachments, including `[]`); typed cards need chip-delete first. Backend `DELETE /treatments/:id` is empty-only (`TREATMENT_HAS_DETAILS`). - Sent-to-lab detail locks that line; **Add detail** still OK same day; **Remove detail** = trash on chip (unsent, including last line; day/edit gates apply). Empty details persist as `[]`. Attachment upload blocked when sent (`TREATMENT_DETAIL_SENT`). - **Entry:** Type dropdown + compact attachments strip (`TreatmentDetailAttachmentsStrip`) on one row, then bordered FDI chart (Cases chrome/scale), then full-width auto-growing **Notes**. No wizard for non-lab types. Prosthesis: `WizardStepper` Treatment → Lab with Back/Next. Detail chips show type + teeth; lab-dependent chips use colored sent/unsent text (sent date on Lab tab). Switching chips resets to the treatment form unless `pendingEntryStepRef` requests Lab (shipments rail). Lab dispatch keeps comments. diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 7412cea..65b9760 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -684,6 +684,8 @@ "purposeLabel": "Purpose:", "loadingAppointments": "Loading appointments…", "selectDayWithAppointment": "Search for a patient or select a visit from the day strip.", + "noTreatmentFoundTitle": "No treatment found", + "noTreatmentFoundBody": "There is no treatment or visit for {name} yet. You can add one with the {action} button in the rail.", "confirmDiscard": "You have unsaved changes. Discard them and continue?", "errorChooseOrg": "Choose at least one active organization to send this case.", "successCaseSent": "Case sent to selected organizations.", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index 9316b36..6909975 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -685,6 +685,8 @@ "purposeLabel": "هدف:", "loadingAppointments": "در حال بارگذاری نوبت‌ها...", "selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.", + "noTreatmentFoundTitle": "درمانی یافت نشد", + "noTreatmentFoundBody": "هنوز درمان یا ویزیتی برای {name} ثبت نشده است. می‌توانید با دکمه {action} در نوار کناری یکی اضافه کنید.", "confirmDiscard": "تغییرات ذخیره‌نشده دارید. آنها را کنار بگذارید و ادامه دهید؟", "errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.", "successCaseSent": "پرونده به سازمان‌های انتخاب شده ارسال شد.", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index 46b7491..a8f2ffb 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -684,6 +684,8 @@ "purposeLabel": "Doel:", "loadingAppointments": "Afspraken laden...", "selectDayWithAppointment": "Zoek een patiënt of kies een bezoek uit de dagstrook.", + "noTreatmentFoundTitle": "Geen behandeling gevonden", + "noTreatmentFoundBody": "Er is nog geen behandeling of bezoek voor {name}. U kunt er een toevoegen met de knop {action} in de zijbalk.", "confirmDiscard": "U heeft niet-opgeslagen wijzigingen. Wilt u deze negeren en doorgaan?", "errorChooseOrg": "Kies ten minste één actieve organisatie om deze case te verzenden.", "successCaseSent": "Case verzonden naar geselecteerde organisaties.", diff --git a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx index b3f56de..586a146 100644 --- a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx +++ b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx @@ -550,6 +550,14 @@ export function TreatmentWorkspace({ email: activePatient.email ?? null, } : null; + const searchedWithoutLiveVisit = Boolean(searchedPatient) && !hasLiveContext; + const showSearchedPatientLoading = + searchedWithoutLiveVisit && (patientSearchBusy || historyLoading); + const showNoTreatmentFound = + searchedWithoutLiveVisit && + !patientSearchBusy && + !historyLoading && + history.length === 0; const unreadUpdatesCount = unreadLabCases.length; const otherPatientsUnreadCount = useMemo( @@ -1570,26 +1578,51 @@ export function TreatmentWorkspace({ return; } + const ok = await flushDraftSave(); + if (!ok) return; + setPatientSearchBusy(true); - setSearchedPatient({ - id: patient.id, - firstName: patient.firstName, - lastName: patient.lastName, - mobile: patient.mobile, - email: patient.email, - }); try { - const response = await treatmentsApi.listPatientHistory(patient.id, 1); - const latest = response.data[0]; - if (latest) { - const ok = await loadTreatmentIntoWorkspace(latest); - if (!ok) { - setSearchedPatient(null); + const stripAppointment = appointments.find((row) => row.patientId === patient.id); + const stripStandalone = standaloneTreatments.find( + (row) => row.patientId === patient.id && !row.patient?.isWalkIn, + ); + if (stripAppointment || stripStandalone) { + draftHydratingRef.current = true; + resetToLiveContext(); + setSearchedPatient(null); + setNewTreatmentPickerOpen(false); + setSelectionLocked(true); + if (stripAppointment) { + setSelectedAppointmentId(stripAppointment.id); + setSelectedStandaloneId(null); + } else if (stripStandalone) { + setSelectedAppointmentId(null); + setSelectedStandaloneId(stripStandalone.id); } return; } - await refreshHistory(patient.id); + setSearchedPatient({ + id: patient.id, + firstName: patient.firstName, + lastName: patient.lastName, + mobile: patient.mobile, + email: patient.email, + }); + resetToLiveContext(); + setNewTreatmentPickerOpen(false); + setSelectedAppointmentId(null); + setSelectedStandaloneId(null); + setSelectionLocked(true); + setHistory([]); + setHistoryLoading(true); + + const response = await treatmentsApi.listPatientHistory(patient.id, 1); + const latest = response.data[0]; + if (latest) { + await loadTreatmentIntoWorkspace(latest); + } } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorLoadHistory'))); setSearchedPatient(null); @@ -1603,8 +1636,11 @@ export function TreatmentWorkspace({ selectedStandalone?.patientId, searchedPatient?.id, hasLiveContext, + flushDraftSave, + appointments, + standaloneTreatments, + resetToLiveContext, loadTreatmentIntoWorkspace, - refreshHistory, showError, t, tErrors, @@ -2371,6 +2407,24 @@ export function TreatmentWorkspace({
+ {showSearchedPatientLoading ? ( +
+

{t('loading')}

+
+ ) : showNoTreatmentFound ? ( +
+

+ {t('noTreatmentFoundTitle')} +

+

+ {t('noTreatmentFoundBody', { + name: activePatientName ?? '', + action: t('newTreatment'), + })} +

+
+ ) : ( + <> ) : null} + + )}