bugfix/treatment-bugs #66
@@ -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 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.
|
- **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.
|
- **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.
|
- **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 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`.
|
- **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`.
|
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 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):**
|
**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`).
|
- **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`).
|
- 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.
|
- **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:",
|
"purposeLabel": "Purpose:",
|
||||||
"loadingAppointments": "Loading appointments…",
|
"loadingAppointments": "Loading appointments…",
|
||||||
"selectDayWithAppointment": "Search for a patient or select a visit from the day strip.",
|
"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?",
|
"confirmDiscard": "You have unsaved changes. Discard them and continue?",
|
||||||
"errorChooseOrg": "Choose at least one active organization to send this case.",
|
"errorChooseOrg": "Choose at least one active organization to send this case.",
|
||||||
"successCaseSent": "Case sent to selected organizations.",
|
"successCaseSent": "Case sent to selected organizations.",
|
||||||
|
|||||||
@@ -685,6 +685,8 @@
|
|||||||
"purposeLabel": "هدف:",
|
"purposeLabel": "هدف:",
|
||||||
"loadingAppointments": "در حال بارگذاری نوبتها...",
|
"loadingAppointments": "در حال بارگذاری نوبتها...",
|
||||||
"selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.",
|
"selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.",
|
||||||
|
"noTreatmentFoundTitle": "درمانی یافت نشد",
|
||||||
|
"noTreatmentFoundBody": "هنوز درمان یا ویزیتی برای {name} ثبت نشده است. میتوانید با دکمه {action} در نوار کناری یکی اضافه کنید.",
|
||||||
"confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
|
"confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
|
||||||
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
|
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
|
||||||
"successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.",
|
"successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.",
|
||||||
|
|||||||
@@ -684,6 +684,8 @@
|
|||||||
"purposeLabel": "Doel:",
|
"purposeLabel": "Doel:",
|
||||||
"loadingAppointments": "Afspraken laden...",
|
"loadingAppointments": "Afspraken laden...",
|
||||||
"selectDayWithAppointment": "Zoek een patiënt of kies een bezoek uit de dagstrook.",
|
"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?",
|
"confirmDiscard": "U heeft niet-opgeslagen wijzigingen. Wilt u deze negeren en doorgaan?",
|
||||||
"errorChooseOrg": "Kies ten minste één actieve organisatie om deze case te verzenden.",
|
"errorChooseOrg": "Kies ten minste één actieve organisatie om deze case te verzenden.",
|
||||||
"successCaseSent": "Case verzonden naar geselecteerde organisaties.",
|
"successCaseSent": "Case verzonden naar geselecteerde organisaties.",
|
||||||
|
|||||||
@@ -550,6 +550,14 @@ export function TreatmentWorkspace({
|
|||||||
email: activePatient.email ?? null,
|
email: activePatient.email ?? null,
|
||||||
}
|
}
|
||||||
: 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 unreadUpdatesCount = unreadLabCases.length;
|
||||||
const otherPatientsUnreadCount = useMemo(
|
const otherPatientsUnreadCount = useMemo(
|
||||||
@@ -1570,7 +1578,31 @@ export function TreatmentWorkspace({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ok = await flushDraftSave();
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
setPatientSearchBusy(true);
|
setPatientSearchBusy(true);
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
setSearchedPatient({
|
setSearchedPatient({
|
||||||
id: patient.id,
|
id: patient.id,
|
||||||
firstName: patient.firstName,
|
firstName: patient.firstName,
|
||||||
@@ -1578,18 +1610,19 @@ export function TreatmentWorkspace({
|
|||||||
mobile: patient.mobile,
|
mobile: patient.mobile,
|
||||||
email: patient.email,
|
email: patient.email,
|
||||||
});
|
});
|
||||||
try {
|
resetToLiveContext();
|
||||||
|
setNewTreatmentPickerOpen(false);
|
||||||
|
setSelectedAppointmentId(null);
|
||||||
|
setSelectedStandaloneId(null);
|
||||||
|
setSelectionLocked(true);
|
||||||
|
setHistory([]);
|
||||||
|
setHistoryLoading(true);
|
||||||
|
|
||||||
const response = await treatmentsApi.listPatientHistory(patient.id, 1);
|
const response = await treatmentsApi.listPatientHistory(patient.id, 1);
|
||||||
const latest = response.data[0];
|
const latest = response.data[0];
|
||||||
if (latest) {
|
if (latest) {
|
||||||
const ok = await loadTreatmentIntoWorkspace(latest);
|
await loadTreatmentIntoWorkspace(latest);
|
||||||
if (!ok) {
|
|
||||||
setSearchedPatient(null);
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshHistory(patient.id);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
||||||
setSearchedPatient(null);
|
setSearchedPatient(null);
|
||||||
@@ -1603,8 +1636,11 @@ export function TreatmentWorkspace({
|
|||||||
selectedStandalone?.patientId,
|
selectedStandalone?.patientId,
|
||||||
searchedPatient?.id,
|
searchedPatient?.id,
|
||||||
hasLiveContext,
|
hasLiveContext,
|
||||||
|
flushDraftSave,
|
||||||
|
appointments,
|
||||||
|
standaloneTreatments,
|
||||||
|
resetToLiveContext,
|
||||||
loadTreatmentIntoWorkspace,
|
loadTreatmentIntoWorkspace,
|
||||||
refreshHistory,
|
|
||||||
showError,
|
showError,
|
||||||
t,
|
t,
|
||||||
tErrors,
|
tErrors,
|
||||||
@@ -2371,6 +2407,24 @@ export function TreatmentWorkspace({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 min-w-0 w-full">
|
<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
|
<TreatmentDetailsEditor
|
||||||
details={details}
|
details={details}
|
||||||
activeDetailId={activeDetailId}
|
activeDetailId={activeDetailId}
|
||||||
@@ -2660,6 +2714,8 @@ export function TreatmentWorkspace({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user