improvement: a flow added to create treatment plan for searched patients with no treatment history.
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -201,7 +201,7 @@ Use shared `Checkbox` (not native `<input type="checkbox">`) 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`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -685,6 +685,8 @@
|
||||
"purposeLabel": "هدف:",
|
||||
"loadingAppointments": "در حال بارگذاری نوبتها...",
|
||||
"selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.",
|
||||
"noTreatmentFoundTitle": "درمانی یافت نشد",
|
||||
"noTreatmentFoundBody": "هنوز درمان یا ویزیتی برای {name} ثبت نشده است. میتوانید با دکمه {action} در نوار کناری یکی اضافه کنید.",
|
||||
"confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
|
||||
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
|
||||
"successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 min-w-0 w-full">
|
||||
{showSearchedPatientLoading ? (
|
||||
<div className="surface-card p-6">
|
||||
<p className="text-sm text-text-muted">{t('loading')}</p>
|
||||
</div>
|
||||
) : showNoTreatmentFound ? (
|
||||
<div className="surface-card w-full p-6 space-y-3">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{t('noTreatmentFoundTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{t('noTreatmentFoundBody', {
|
||||
name: activePatientName ?? '',
|
||||
action: t('newTreatment'),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<TreatmentDetailsEditor
|
||||
details={details}
|
||||
activeDetailId={activeDetailId}
|
||||
@@ -2660,6 +2714,8 @@ export function TreatmentWorkspace({
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user