bugfix/treatment-bugs #66

Merged
admin merged 6 commits from bugfix/treatment-bugs into master 2026-08-23 12:44:46 +03:30
7 changed files with 80 additions and 18 deletions
Showing only changes of commit 7f02a84ebf - Show all commits

View File

@@ -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 todays 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`.

View File

@@ -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 todays 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`.

View File

@@ -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 cards 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 cards 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 details 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.

View File

@@ -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.",

View File

@@ -685,6 +685,8 @@
"purposeLabel": "هدف:",
"loadingAppointments": "در حال بارگذاری نوبت‌ها...",
"selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.",
"noTreatmentFoundTitle": "درمانی یافت نشد",
"noTreatmentFoundBody": "هنوز درمان یا ویزیتی برای {name} ثبت نشده است. می‌توانید با دکمه {action} در نوار کناری یکی اضافه کنید.",
"confirmDiscard": "تغییرات ذخیره‌نشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
"successCaseSent": "پرونده به سازمان‌های انتخاب شده ارسال شد.",

View File

@@ -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.",

View File

@@ -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>