bugfix/treatment-bugs #66
@@ -14,6 +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.
|
||||||
- **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`.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Thin route: `app/[locale]/(dashboard)/treatment/page.tsx` (supports `?appointmen
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
1. **Day strip** — `AppointmentsStrip.tsx` renders `DayStripItem[]` (`appointment` | `unscheduled`) via `DayStripCard`. Header uses **`ScheduleDayPicker` `compact`**: date is centered in a 3-col grid; no “Schedule date” label; **Today** sits on the navigator (`CalendarDaySelect` when the label row is hidden). Timed appointments keep treatment-type pastel banners. Unscheduled cards use the same banner from the **first detail’s type only** (`unscheduledStripColorCode`; live draft for the open card; `draftHydratingRef` must be set **before** strip/appointment pick so overlay does not paint the previous card’s type). Empty first line → chip theming even if later lines are typed. Trash inherits banner ink on typed cards. Strip trash only when `areUnscheduledDetailsStripDeletable` (no type/teeth/notes/attachments, including `[]`). Workspace fetches `GET /appointments` **and** `GET /treatments/day`. **New treatment** is one shared `Button`: it opens `NewTreatmentPatientPicker` (Walk-in always first, then search). Creating happens only after an explicit patient choice — never from the selected appointment card. New treatment seeds one blank detail so the type field is ready; a persisted empty plan hydrates as `[]` until Add (`noDetails` copy — not the type-first overlay). Last-line chip delete confirms the plan will be empty until Add.
|
1. **Day strip** — `AppointmentsStrip.tsx` renders `DayStripItem[]` (`appointment` | `unscheduled`) via `DayStripCard`. Header uses **`ScheduleDayPicker` `compact`**: date is centered in a 3-col grid; no “Schedule date” label; **Today** sits on the navigator (`CalendarDaySelect` when the label row is hidden). Timed appointments keep treatment-type pastel banners. Unscheduled cards use the same banner from the **first detail’s type only** (`unscheduledStripColorCode`; live draft for the open card; `draftHydratingRef` must be set **before** strip/appointment pick so overlay does not paint the previous card’s type). Empty first line → chip theming even if later lines are typed. Trash inherits banner ink on typed cards. Strip trash only when `areUnscheduledDetailsStripDeletable` (no type/teeth/notes/attachments, including `[]`). Workspace fetches `GET /appointments` **and** `GET /treatments/day`. Patient search (`PatientSearchCombobox`) sits in the **page header** (workspace-wide). **New treatment** is one shared `Button` at the **top of the left rail**, with the selected-patient card under it: it opens `NewTreatmentPatientPicker` (Walk-in always first, then a matching full-width card for the current named patient with name + mobile/email or hint, then search). Creating happens only after an explicit patient choice — never from the selected appointment card. New treatment seeds one blank detail so the type field is ready; a persisted empty plan hydrates as `[]` until Add (`noDetails` copy — not the type-first overlay). Last-line chip delete confirms the plan will be empty until Add.
|
||||||
|
|
||||||
2. **Treatment preview** — `TreatmentPreviewCard.tsx` (history browse only; omitted for the live draft)
|
2. **Treatment preview** — `TreatmentPreviewCard.tsx` (history browse only; omitted for the live draft)
|
||||||
|
|
||||||
@@ -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 a patient, including Walk-in).
|
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).
|
||||||
|
|
||||||
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. **New treatment** opens a patient picker (Walk-in always visible); it does not copy the selected appointment’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). **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.
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ type SentLabCaseRow = Prisma.LabCaseGetPayload<{ include: typeof sentLabCaseIncl
|
|||||||
|
|
||||||
const treatmentInclude = {
|
const treatmentInclude = {
|
||||||
patient: {
|
patient: {
|
||||||
select: { id: true, firstName: true, lastName: true, isWalkIn: true },
|
select: { id: true, firstName: true, lastName: true, isWalkIn: true, mobile: true, email: true },
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
orderBy: [{ sortOrder: 'asc' as const }],
|
orderBy: [{ sortOrder: 'asc' as const }],
|
||||||
@@ -1366,6 +1366,8 @@ export class TreatmentsService {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
isWalkIn: boolean;
|
isWalkIn: boolean;
|
||||||
|
mobile?: string | null;
|
||||||
|
email?: string | null;
|
||||||
};
|
};
|
||||||
details: Array<{
|
details: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1426,6 +1428,8 @@ export class TreatmentsService {
|
|||||||
firstName: treatment.patient.firstName,
|
firstName: treatment.patient.firstName,
|
||||||
lastName: treatment.patient.lastName,
|
lastName: treatment.patient.lastName,
|
||||||
isWalkIn: treatment.patient.isWalkIn,
|
isWalkIn: treatment.patient.isWalkIn,
|
||||||
|
mobile: treatment.patient.mobile ?? null,
|
||||||
|
email: treatment.patient.email ?? null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
details: treatment.details.map((d) => this.mapDetail(d)),
|
details: treatment.details.map((d) => this.mapDetail(d)),
|
||||||
|
|||||||
@@ -683,7 +683,7 @@
|
|||||||
"selectedPatient": "Selected patient",
|
"selectedPatient": "Selected patient",
|
||||||
"purposeLabel": "Purpose:",
|
"purposeLabel": "Purpose:",
|
||||||
"loadingAppointments": "Loading appointments…",
|
"loadingAppointments": "Loading appointments…",
|
||||||
"selectDayWithAppointment": "Select a day with at least one appointment.",
|
"selectDayWithAppointment": "Search for a patient or select a visit from the day strip.",
|
||||||
"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.",
|
||||||
@@ -707,6 +707,8 @@
|
|||||||
"walkIn": "Walk-in",
|
"walkIn": "Walk-in",
|
||||||
"newTreatment": "New treatment",
|
"newTreatment": "New treatment",
|
||||||
"newTreatmentPatientPrompt": "Who is this visit for?",
|
"newTreatmentPatientPrompt": "Who is this visit for?",
|
||||||
|
"newTreatmentUseCurrent": "Use {name}",
|
||||||
|
"newTreatmentUseCurrentHint": "Start a new visit for the patient already open on this page.",
|
||||||
"walkInPickerHint": "No named patient — always available, no search needed.",
|
"walkInPickerHint": "No named patient — always available, no search needed.",
|
||||||
"errorCreateTreatment": "Could not create treatment.",
|
"errorCreateTreatment": "Could not create treatment.",
|
||||||
"deleteEmptyTreatment": "Delete empty treatment",
|
"deleteEmptyTreatment": "Delete empty treatment",
|
||||||
|
|||||||
@@ -684,7 +684,7 @@
|
|||||||
"selectedPatient": "بیمار انتخاب شده",
|
"selectedPatient": "بیمار انتخاب شده",
|
||||||
"purposeLabel": "هدف:",
|
"purposeLabel": "هدف:",
|
||||||
"loadingAppointments": "در حال بارگذاری نوبتها...",
|
"loadingAppointments": "در حال بارگذاری نوبتها...",
|
||||||
"selectDayWithAppointment": "روزی را انتخاب کنید که حداقل یک نوبت داشته باشد.",
|
"selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.",
|
||||||
"confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
|
"confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
|
||||||
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
|
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
|
||||||
"successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.",
|
"successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.",
|
||||||
@@ -708,6 +708,8 @@
|
|||||||
"walkIn": "بدون نوبت (مراجع)",
|
"walkIn": "بدون نوبت (مراجع)",
|
||||||
"newTreatment": "درمان جدید",
|
"newTreatment": "درمان جدید",
|
||||||
"newTreatmentPatientPrompt": "این ویزیت برای چه کسی است؟",
|
"newTreatmentPatientPrompt": "این ویزیت برای چه کسی است؟",
|
||||||
|
"newTreatmentUseCurrent": "استفاده از {name}",
|
||||||
|
"newTreatmentUseCurrentHint": "ویزیت جدیدی برای بیماری که همین حالا در این صفحه باز است شروع کنید.",
|
||||||
"walkInPickerHint": "بیمار نامدار نیست — همیشه در دسترس است و نیازی به جستجو ندارد.",
|
"walkInPickerHint": "بیمار نامدار نیست — همیشه در دسترس است و نیازی به جستجو ندارد.",
|
||||||
"errorCreateTreatment": "ایجاد درمان ممکن نشد.",
|
"errorCreateTreatment": "ایجاد درمان ممکن نشد.",
|
||||||
"deleteEmptyTreatment": "حذف درمان خالی",
|
"deleteEmptyTreatment": "حذف درمان خالی",
|
||||||
|
|||||||
@@ -683,7 +683,7 @@
|
|||||||
"selectedPatient": "Geselecteerde patiënt",
|
"selectedPatient": "Geselecteerde patiënt",
|
||||||
"purposeLabel": "Doel:",
|
"purposeLabel": "Doel:",
|
||||||
"loadingAppointments": "Afspraken laden...",
|
"loadingAppointments": "Afspraken laden...",
|
||||||
"selectDayWithAppointment": "Selecteer een dag met ten minste één afspraak.",
|
"selectDayWithAppointment": "Zoek een patiënt of kies een bezoek uit de dagstrook.",
|
||||||
"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.",
|
||||||
@@ -707,6 +707,8 @@
|
|||||||
"walkIn": "Inloop",
|
"walkIn": "Inloop",
|
||||||
"newTreatment": "Nieuwe behandeling",
|
"newTreatment": "Nieuwe behandeling",
|
||||||
"newTreatmentPatientPrompt": "Voor wie is dit bezoek?",
|
"newTreatmentPatientPrompt": "Voor wie is dit bezoek?",
|
||||||
|
"newTreatmentUseCurrent": "{name} gebruiken",
|
||||||
|
"newTreatmentUseCurrentHint": "Start een nieuw bezoek voor de patiënt die al op deze pagina openstaat.",
|
||||||
"walkInPickerHint": "Geen benoemde patiënt — altijd beschikbaar, zonder zoeken.",
|
"walkInPickerHint": "Geen benoemde patiënt — altijd beschikbaar, zonder zoeken.",
|
||||||
"errorCreateTreatment": "Behandeling aanmaken is mislukt.",
|
"errorCreateTreatment": "Behandeling aanmaken is mislukt.",
|
||||||
"deleteEmptyTreatment": "Lege behandeling verwijderen",
|
"deleteEmptyTreatment": "Lege behandeling verwijderen",
|
||||||
|
|||||||
@@ -4,19 +4,45 @@ import { useTranslations } from 'next-intl';
|
|||||||
import { Button } from '@/components/ui/shared/Button';
|
import { Button } from '@/components/ui/shared/Button';
|
||||||
import { PatientSearchCombobox } from '@/components/ui/patient/PatientSearchCombobox';
|
import { PatientSearchCombobox } from '@/components/ui/patient/PatientSearchCombobox';
|
||||||
import { usePatientSearchQuery } from '@/lib/hooks/usePatientSearchQuery';
|
import { usePatientSearchQuery } from '@/lib/hooks/usePatientSearchQuery';
|
||||||
|
import { formatMobileForDisplay } from '@/lib/phone';
|
||||||
import type { Patient } from '@/types/patient';
|
import type { Patient } from '@/types/patient';
|
||||||
|
|
||||||
|
const pickerChoiceClass =
|
||||||
|
'w-full rounded-[var(--radius-md)] border border-primary/40 bg-primary-soft px-3 py-2 text-start transition-colors hover:border-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45';
|
||||||
|
|
||||||
interface NewTreatmentPatientPickerProps {
|
interface NewTreatmentPatientPickerProps {
|
||||||
creating?: boolean;
|
creating?: boolean;
|
||||||
|
currentPatient?: {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
mobile?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
} | null;
|
||||||
onSelectWalkIn: () => void | Promise<void>;
|
onSelectWalkIn: () => void | Promise<void>;
|
||||||
onSelectPatient: (patient: Patient) => void | Promise<void>;
|
onSelectPatient: (patient: Patient) => void | Promise<void>;
|
||||||
|
onSelectCurrentPatient?: () => void | Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function contactLine(
|
||||||
|
patient: { mobile?: string | null; email?: string | null },
|
||||||
|
fallback: string,
|
||||||
|
): string {
|
||||||
|
const mobile = patient.mobile?.trim()
|
||||||
|
? formatMobileForDisplay(patient.mobile.trim())
|
||||||
|
: '';
|
||||||
|
if (mobile) return mobile;
|
||||||
|
const email = patient.email?.trim();
|
||||||
|
if (email) return email;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export function NewTreatmentPatientPicker({
|
export function NewTreatmentPatientPicker({
|
||||||
creating = false,
|
creating = false,
|
||||||
|
currentPatient = null,
|
||||||
onSelectWalkIn,
|
onSelectWalkIn,
|
||||||
onSelectPatient,
|
onSelectPatient,
|
||||||
|
onSelectCurrentPatient,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: NewTreatmentPatientPickerProps) {
|
}: NewTreatmentPatientPickerProps) {
|
||||||
const t = useTranslations('treatment');
|
const t = useTranslations('treatment');
|
||||||
@@ -31,11 +57,25 @@ export function NewTreatmentPatientPicker({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
onClick={() => void onSelectWalkIn()}
|
onClick={() => void onSelectWalkIn()}
|
||||||
className="w-full rounded-[var(--radius-md)] border border-primary/40 bg-primary-soft px-3 py-2 text-start transition-colors hover:border-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45"
|
className={pickerChoiceClass}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium text-text-primary">{t('walkIn')}</p>
|
<p className="text-sm font-medium text-text-primary">{t('walkIn')}</p>
|
||||||
<p className="text-xs text-text-muted mt-0.5">{t('walkInPickerHint')}</p>
|
<p className="text-xs text-text-muted mt-0.5">{t('walkInPickerHint')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
{currentPatient && onSelectCurrentPatient ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={creating}
|
||||||
|
onClick={() => void onSelectCurrentPatient()}
|
||||||
|
aria-label={t('newTreatmentUseCurrent', { name: currentPatient.displayName })}
|
||||||
|
className={pickerChoiceClass}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium text-text-primary break-words">{currentPatient.displayName}</p>
|
||||||
|
<p className="text-xs text-text-muted mt-0.5 break-words">
|
||||||
|
{contactLine(currentPatient, t('newTreatmentUseCurrentHint'))}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<PatientSearchCombobox
|
<PatientSearchCombobox
|
||||||
search={search}
|
search={search}
|
||||||
onSearchChange={setSearch}
|
onSearchChange={setSearch}
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ function mapAppointment(record: AppointmentRecord): TreatmentAppointment {
|
|||||||
patientId: record.patientId,
|
patientId: record.patientId,
|
||||||
patientFirstName: record.patient.firstName,
|
patientFirstName: record.patient.firstName,
|
||||||
patientLastName: record.patient.lastName,
|
patientLastName: record.patient.lastName,
|
||||||
|
patientMobile: record.patient.mobile,
|
||||||
providerUserId: record.providerUserId,
|
providerUserId: record.providerUserId,
|
||||||
startAt: record.startAt,
|
startAt: record.startAt,
|
||||||
endAt: record.endAt,
|
endAt: record.endAt,
|
||||||
@@ -386,10 +387,12 @@ export function TreatmentWorkspace({
|
|||||||
const [unreadLabCasesLoading, setUnreadLabCasesLoading] = useState(false);
|
const [unreadLabCasesLoading, setUnreadLabCasesLoading] = useState(false);
|
||||||
const [labCasesScope, setLabCasesScope] = useState<TreatmentLabCasesScope>('patient');
|
const [labCasesScope, setLabCasesScope] = useState<TreatmentLabCasesScope>('patient');
|
||||||
const [selectedRailLabCaseId, setSelectedRailLabCaseId] = useState<string | null>(null);
|
const [selectedRailLabCaseId, setSelectedRailLabCaseId] = useState<string | null>(null);
|
||||||
const [searchedPatient, setSearchedPatient] = useState<Pick<
|
const [searchedPatient, setSearchedPatient] = useState<
|
||||||
Patient,
|
(Pick<Patient, 'id' | 'firstName' | 'lastName'> & {
|
||||||
'id' | 'firstName' | 'lastName'
|
mobile?: string | null;
|
||||||
> | null>(null);
|
email?: string | null;
|
||||||
|
}) | null
|
||||||
|
>(null);
|
||||||
const [patientSearchBusy, setPatientSearchBusy] = useState(false);
|
const [patientSearchBusy, setPatientSearchBusy] = useState(false);
|
||||||
const [newTreatmentPickerOpen, setNewTreatmentPickerOpen] = useState(false);
|
const [newTreatmentPickerOpen, setNewTreatmentPickerOpen] = useState(false);
|
||||||
const [creatingStandalone, setCreatingStandalone] = useState(false);
|
const [creatingStandalone, setCreatingStandalone] = useState(false);
|
||||||
@@ -502,6 +505,8 @@ export function TreatmentWorkspace({
|
|||||||
id: selectedAppointment.patientId,
|
id: selectedAppointment.patientId,
|
||||||
firstName: selectedAppointment.patientFirstName,
|
firstName: selectedAppointment.patientFirstName,
|
||||||
lastName: selectedAppointment.patientLastName,
|
lastName: selectedAppointment.patientLastName,
|
||||||
|
mobile: selectedAppointment.patientMobile ?? null,
|
||||||
|
email: null,
|
||||||
purpose: selectedAppointment.purpose,
|
purpose: selectedAppointment.purpose,
|
||||||
isWalkIn: false,
|
isWalkIn: false,
|
||||||
};
|
};
|
||||||
@@ -512,6 +517,8 @@ export function TreatmentWorkspace({
|
|||||||
id: selectedStandalone.patientId,
|
id: selectedStandalone.patientId,
|
||||||
firstName: isWalkIn ? walkInLabel : (selectedStandalone.patient?.firstName ?? ''),
|
firstName: isWalkIn ? walkInLabel : (selectedStandalone.patient?.firstName ?? ''),
|
||||||
lastName: isWalkIn ? '' : (selectedStandalone.patient?.lastName ?? ''),
|
lastName: isWalkIn ? '' : (selectedStandalone.patient?.lastName ?? ''),
|
||||||
|
mobile: isWalkIn ? null : (selectedStandalone.patient?.mobile ?? null),
|
||||||
|
email: isWalkIn ? null : (selectedStandalone.patient?.email ?? null),
|
||||||
purpose: selectedStandalone.details[0]?.treatmentType,
|
purpose: selectedStandalone.details[0]?.treatmentType,
|
||||||
isWalkIn,
|
isWalkIn,
|
||||||
};
|
};
|
||||||
@@ -521,7 +528,9 @@ export function TreatmentWorkspace({
|
|||||||
id: searchedPatient.id,
|
id: searchedPatient.id,
|
||||||
firstName: searchedPatient.firstName,
|
firstName: searchedPatient.firstName,
|
||||||
lastName: searchedPatient.lastName,
|
lastName: searchedPatient.lastName,
|
||||||
purpose: undefined as string | undefined,
|
mobile: searchedPatient.mobile ?? null,
|
||||||
|
email: searchedPatient.email ?? null,
|
||||||
|
purpose: undefined,
|
||||||
isWalkIn: false,
|
isWalkIn: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -532,6 +541,15 @@ export function TreatmentWorkspace({
|
|||||||
const activePatientName = activePatient
|
const activePatientName = activePatient
|
||||||
? `${activePatient.firstName} ${activePatient.lastName}`.trim()
|
? `${activePatient.firstName} ${activePatient.lastName}`.trim()
|
||||||
: null;
|
: null;
|
||||||
|
const namedActivePatient =
|
||||||
|
activePatient && !activePatient.isWalkIn && activePatient.id
|
||||||
|
? {
|
||||||
|
id: activePatient.id,
|
||||||
|
displayName: activePatientName ?? '',
|
||||||
|
mobile: activePatient.mobile ?? null,
|
||||||
|
email: activePatient.email ?? null,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
const unreadUpdatesCount = unreadLabCases.length;
|
const unreadUpdatesCount = unreadLabCases.length;
|
||||||
const otherPatientsUnreadCount = useMemo(
|
const otherPatientsUnreadCount = useMemo(
|
||||||
@@ -1557,6 +1575,8 @@ export function TreatmentWorkspace({
|
|||||||
id: patient.id,
|
id: patient.id,
|
||||||
firstName: patient.firstName,
|
firstName: patient.firstName,
|
||||||
lastName: patient.lastName,
|
lastName: patient.lastName,
|
||||||
|
mobile: patient.mobile,
|
||||||
|
email: patient.email,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const response = await treatmentsApi.listPatientHistory(patient.id, 1);
|
const response = await treatmentsApi.listPatientHistory(patient.id, 1);
|
||||||
@@ -2146,11 +2166,24 @@ export function TreatmentWorkspace({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<header className="space-y-1">
|
<header className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
|
||||||
|
<div className="min-w-0 shrink-0">
|
||||||
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary">{t('title')}</h1>
|
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary">{t('title')}</h1>
|
||||||
{!canEdit ? (
|
{!canEdit ? (
|
||||||
<p className="text-sm text-text-secondary">{t('subtitleReadOnly')}</p>
|
<p className="text-sm text-text-secondary mt-1">{t('subtitleReadOnly')}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<PatientSearchCombobox
|
||||||
|
search={patientSearch}
|
||||||
|
onSearchChange={setPatientSearch}
|
||||||
|
patients={patientSearchResults}
|
||||||
|
loading={patientSearchLoading || patientSearchBusy}
|
||||||
|
onSelectPatient={handleSelectSearchedPatient}
|
||||||
|
placeholder={tPatients('searchPlaceholder')}
|
||||||
|
emptyResultsMessage={tPatients('noResults')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<AppointmentsStrip
|
<AppointmentsStrip
|
||||||
@@ -2181,15 +2214,6 @@ export function TreatmentWorkspace({
|
|||||||
<div className="treatment-layout-grid grid grid-cols-1 xl:grid-cols-[minmax(300px,380px)_minmax(0,1fr)] gap-4 items-start">
|
<div className="treatment-layout-grid grid grid-cols-1 xl:grid-cols-[minmax(300px,380px)_minmax(0,1fr)] gap-4 items-start">
|
||||||
<div className="space-y-3 min-w-0 xl:max-w-[380px]">
|
<div className="space-y-3 min-w-0 xl:max-w-[380px]">
|
||||||
<div className="surface-card p-3 space-y-3">
|
<div className="surface-card p-3 space-y-3">
|
||||||
<PatientSearchCombobox
|
|
||||||
search={patientSearch}
|
|
||||||
onSearchChange={setPatientSearch}
|
|
||||||
patients={patientSearchResults}
|
|
||||||
loading={patientSearchLoading || patientSearchBusy}
|
|
||||||
onSelectPatient={handleSelectSearchedPatient}
|
|
||||||
placeholder={tPatients('searchPlaceholder')}
|
|
||||||
emptyResultsMessage={tPatients('noResults')}
|
|
||||||
/>
|
|
||||||
{canEdit && !isViewingPastDay ? (
|
{canEdit && !isViewingPastDay ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -2204,10 +2228,16 @@ export function TreatmentWorkspace({
|
|||||||
{newTreatmentPickerOpen ? (
|
{newTreatmentPickerOpen ? (
|
||||||
<NewTreatmentPatientPicker
|
<NewTreatmentPatientPicker
|
||||||
creating={creatingStandalone}
|
creating={creatingStandalone}
|
||||||
|
currentPatient={namedActivePatient}
|
||||||
onSelectWalkIn={() => createStandaloneTreatment({ walkIn: true })}
|
onSelectWalkIn={() => createStandaloneTreatment({ walkIn: true })}
|
||||||
onSelectPatient={(patient) =>
|
onSelectPatient={(patient) =>
|
||||||
createStandaloneTreatment({ patientId: patient.id })
|
createStandaloneTreatment({ patientId: patient.id })
|
||||||
}
|
}
|
||||||
|
onSelectCurrentPatient={
|
||||||
|
namedActivePatient
|
||||||
|
? () => createStandaloneTreatment({ patientId: namedActivePatient.id })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onCancel={() => setNewTreatmentPickerOpen(false)}
|
onCancel={() => setNewTreatmentPickerOpen(false)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -2215,8 +2245,14 @@ export function TreatmentWorkspace({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{activePatient ? (
|
{activePatient ? (
|
||||||
<div className="space-y-0.5 border-t border-border/60 pt-3">
|
<div
|
||||||
<p className="text-[10px] uppercase tracking-wide text-text-muted">{t('selectedPatient')}</p>
|
className={`space-y-0.5 ${
|
||||||
|
canEdit && !isViewingPastDay ? 'border-t border-border/60 pt-3' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-[10px] uppercase tracking-wide text-text-muted">
|
||||||
|
{t('selectedPatient')}
|
||||||
|
</p>
|
||||||
<p className="text-base font-semibold text-text-primary">{activePatientName}</p>
|
<p className="text-base font-semibold text-text-primary">{activePatientName}</p>
|
||||||
{activePatient.purpose ? (
|
{activePatient.purpose ? (
|
||||||
<p className="text-[11px] text-text-secondary">
|
<p className="text-[11px] text-text-secondary">
|
||||||
@@ -2228,7 +2264,11 @@ export function TreatmentWorkspace({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-text-muted border-t border-border/60 pt-3">
|
<p
|
||||||
|
className={`text-sm text-text-muted ${
|
||||||
|
canEdit && !isViewingPastDay ? 'border-t border-border/60 pt-3' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{apptsLoading ? t('loadingAppointments') : t('selectDayWithAppointment')}
|
{apptsLoading ? t('loadingAppointments') : t('selectDayWithAppointment')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface TreatmentAppointment {
|
|||||||
patientId: string;
|
patientId: string;
|
||||||
patientFirstName: string;
|
patientFirstName: string;
|
||||||
patientLastName: string;
|
patientLastName: string;
|
||||||
|
patientMobile?: string | null;
|
||||||
providerUserId: string;
|
providerUserId: string;
|
||||||
startAt: string;
|
startAt: string;
|
||||||
endAt: string;
|
endAt: string;
|
||||||
@@ -139,6 +140,8 @@ export interface PastTreatment {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
isWalkIn: boolean;
|
isWalkIn: boolean;
|
||||||
|
mobile?: string | null;
|
||||||
|
email?: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
details: PastTreatmentDetail[];
|
details: PastTreatmentDetail[];
|
||||||
labCases: PastLabCase[];
|
labCases: PastLabCase[];
|
||||||
|
|||||||
Reference in New Issue
Block a user