improvement: patient search moved to the top of treatment feature.
This commit is contained in:
@@ -683,7 +683,7 @@
|
||||
"selectedPatient": "Selected patient",
|
||||
"purposeLabel": "Purpose:",
|
||||
"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?",
|
||||
"errorChooseOrg": "Choose at least one active organization to send this case.",
|
||||
"successCaseSent": "Case sent to selected organizations.",
|
||||
@@ -707,6 +707,8 @@
|
||||
"walkIn": "Walk-in",
|
||||
"newTreatment": "New treatment",
|
||||
"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.",
|
||||
"errorCreateTreatment": "Could not create treatment.",
|
||||
"deleteEmptyTreatment": "Delete empty treatment",
|
||||
|
||||
@@ -684,7 +684,7 @@
|
||||
"selectedPatient": "بیمار انتخاب شده",
|
||||
"purposeLabel": "هدف:",
|
||||
"loadingAppointments": "در حال بارگذاری نوبتها...",
|
||||
"selectDayWithAppointment": "روزی را انتخاب کنید که حداقل یک نوبت داشته باشد.",
|
||||
"selectDayWithAppointment": "برای بیمار جستجو کنید یا یک ویزیت را از نوار روز انتخاب کنید.",
|
||||
"confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟",
|
||||
"errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.",
|
||||
"successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.",
|
||||
@@ -708,6 +708,8 @@
|
||||
"walkIn": "بدون نوبت (مراجع)",
|
||||
"newTreatment": "درمان جدید",
|
||||
"newTreatmentPatientPrompt": "این ویزیت برای چه کسی است؟",
|
||||
"newTreatmentUseCurrent": "استفاده از {name}",
|
||||
"newTreatmentUseCurrentHint": "ویزیت جدیدی برای بیماری که همین حالا در این صفحه باز است شروع کنید.",
|
||||
"walkInPickerHint": "بیمار نامدار نیست — همیشه در دسترس است و نیازی به جستجو ندارد.",
|
||||
"errorCreateTreatment": "ایجاد درمان ممکن نشد.",
|
||||
"deleteEmptyTreatment": "حذف درمان خالی",
|
||||
|
||||
@@ -683,7 +683,7 @@
|
||||
"selectedPatient": "Geselecteerde patiënt",
|
||||
"purposeLabel": "Doel:",
|
||||
"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?",
|
||||
"errorChooseOrg": "Kies ten minste één actieve organisatie om deze case te verzenden.",
|
||||
"successCaseSent": "Case verzonden naar geselecteerde organisaties.",
|
||||
@@ -707,6 +707,8 @@
|
||||
"walkIn": "Inloop",
|
||||
"newTreatment": "Nieuwe behandeling",
|
||||
"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.",
|
||||
"errorCreateTreatment": "Behandeling aanmaken is mislukt.",
|
||||
"deleteEmptyTreatment": "Lege behandeling verwijderen",
|
||||
|
||||
@@ -4,19 +4,45 @@ import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { PatientSearchCombobox } from '@/components/ui/patient/PatientSearchCombobox';
|
||||
import { usePatientSearchQuery } from '@/lib/hooks/usePatientSearchQuery';
|
||||
import { formatMobileForDisplay } from '@/lib/phone';
|
||||
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 {
|
||||
creating?: boolean;
|
||||
currentPatient?: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
mobile?: string | null;
|
||||
email?: string | null;
|
||||
} | null;
|
||||
onSelectWalkIn: () => void | Promise<void>;
|
||||
onSelectPatient: (patient: Patient) => void | Promise<void>;
|
||||
onSelectCurrentPatient?: () => void | Promise<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({
|
||||
creating = false,
|
||||
currentPatient = null,
|
||||
onSelectWalkIn,
|
||||
onSelectPatient,
|
||||
onSelectCurrentPatient,
|
||||
onCancel,
|
||||
}: NewTreatmentPatientPickerProps) {
|
||||
const t = useTranslations('treatment');
|
||||
@@ -31,11 +57,25 @@ export function NewTreatmentPatientPicker({
|
||||
type="button"
|
||||
disabled={creating}
|
||||
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-xs text-text-muted mt-0.5">{t('walkInPickerHint')}</p>
|
||||
</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
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
|
||||
@@ -206,6 +206,7 @@ function mapAppointment(record: AppointmentRecord): TreatmentAppointment {
|
||||
patientId: record.patientId,
|
||||
patientFirstName: record.patient.firstName,
|
||||
patientLastName: record.patient.lastName,
|
||||
patientMobile: record.patient.mobile,
|
||||
providerUserId: record.providerUserId,
|
||||
startAt: record.startAt,
|
||||
endAt: record.endAt,
|
||||
@@ -386,10 +387,12 @@ export function TreatmentWorkspace({
|
||||
const [unreadLabCasesLoading, setUnreadLabCasesLoading] = useState(false);
|
||||
const [labCasesScope, setLabCasesScope] = useState<TreatmentLabCasesScope>('patient');
|
||||
const [selectedRailLabCaseId, setSelectedRailLabCaseId] = useState<string | null>(null);
|
||||
const [searchedPatient, setSearchedPatient] = useState<Pick<
|
||||
Patient,
|
||||
'id' | 'firstName' | 'lastName'
|
||||
> | null>(null);
|
||||
const [searchedPatient, setSearchedPatient] = useState<
|
||||
(Pick<Patient, 'id' | 'firstName' | 'lastName'> & {
|
||||
mobile?: string | null;
|
||||
email?: string | null;
|
||||
}) | null
|
||||
>(null);
|
||||
const [patientSearchBusy, setPatientSearchBusy] = useState(false);
|
||||
const [newTreatmentPickerOpen, setNewTreatmentPickerOpen] = useState(false);
|
||||
const [creatingStandalone, setCreatingStandalone] = useState(false);
|
||||
@@ -502,6 +505,8 @@ export function TreatmentWorkspace({
|
||||
id: selectedAppointment.patientId,
|
||||
firstName: selectedAppointment.patientFirstName,
|
||||
lastName: selectedAppointment.patientLastName,
|
||||
mobile: selectedAppointment.patientMobile ?? null,
|
||||
email: null,
|
||||
purpose: selectedAppointment.purpose,
|
||||
isWalkIn: false,
|
||||
};
|
||||
@@ -512,6 +517,8 @@ export function TreatmentWorkspace({
|
||||
id: selectedStandalone.patientId,
|
||||
firstName: isWalkIn ? walkInLabel : (selectedStandalone.patient?.firstName ?? ''),
|
||||
lastName: isWalkIn ? '' : (selectedStandalone.patient?.lastName ?? ''),
|
||||
mobile: isWalkIn ? null : (selectedStandalone.patient?.mobile ?? null),
|
||||
email: isWalkIn ? null : (selectedStandalone.patient?.email ?? null),
|
||||
purpose: selectedStandalone.details[0]?.treatmentType,
|
||||
isWalkIn,
|
||||
};
|
||||
@@ -521,7 +528,9 @@ export function TreatmentWorkspace({
|
||||
id: searchedPatient.id,
|
||||
firstName: searchedPatient.firstName,
|
||||
lastName: searchedPatient.lastName,
|
||||
purpose: undefined as string | undefined,
|
||||
mobile: searchedPatient.mobile ?? null,
|
||||
email: searchedPatient.email ?? null,
|
||||
purpose: undefined,
|
||||
isWalkIn: false,
|
||||
};
|
||||
}
|
||||
@@ -532,6 +541,15 @@ export function TreatmentWorkspace({
|
||||
const activePatientName = activePatient
|
||||
? `${activePatient.firstName} ${activePatient.lastName}`.trim()
|
||||
: 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 otherPatientsUnreadCount = useMemo(
|
||||
@@ -1557,6 +1575,8 @@ export function TreatmentWorkspace({
|
||||
id: patient.id,
|
||||
firstName: patient.firstName,
|
||||
lastName: patient.lastName,
|
||||
mobile: patient.mobile,
|
||||
email: patient.email,
|
||||
});
|
||||
try {
|
||||
const response = await treatmentsApi.listPatientHistory(patient.id, 1);
|
||||
@@ -2146,11 +2166,24 @@ export function TreatmentWorkspace({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary">{t('title')}</h1>
|
||||
{!canEdit ? (
|
||||
<p className="text-sm text-text-secondary">{t('subtitleReadOnly')}</p>
|
||||
) : null}
|
||||
<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>
|
||||
{!canEdit ? (
|
||||
<p className="text-sm text-text-secondary mt-1">{t('subtitleReadOnly')}</p>
|
||||
) : 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>
|
||||
|
||||
<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="space-y-3 min-w-0 xl:max-w-[380px]">
|
||||
<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 ? (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
@@ -2204,10 +2228,16 @@ export function TreatmentWorkspace({
|
||||
{newTreatmentPickerOpen ? (
|
||||
<NewTreatmentPatientPicker
|
||||
creating={creatingStandalone}
|
||||
currentPatient={namedActivePatient}
|
||||
onSelectWalkIn={() => createStandaloneTreatment({ walkIn: true })}
|
||||
onSelectPatient={(patient) =>
|
||||
createStandaloneTreatment({ patientId: patient.id })
|
||||
}
|
||||
onSelectCurrentPatient={
|
||||
namedActivePatient
|
||||
? () => createStandaloneTreatment({ patientId: namedActivePatient.id })
|
||||
: undefined
|
||||
}
|
||||
onCancel={() => setNewTreatmentPickerOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
@@ -2215,8 +2245,14 @@ export function TreatmentWorkspace({
|
||||
) : null}
|
||||
|
||||
{activePatient ? (
|
||||
<div className="space-y-0.5 border-t border-border/60 pt-3">
|
||||
<p className="text-[10px] uppercase tracking-wide text-text-muted">{t('selectedPatient')}</p>
|
||||
<div
|
||||
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>
|
||||
{activePatient.purpose ? (
|
||||
<p className="text-[11px] text-text-secondary">
|
||||
@@ -2228,7 +2264,11 @@ export function TreatmentWorkspace({
|
||||
) : null}
|
||||
</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')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface TreatmentAppointment {
|
||||
patientId: string;
|
||||
patientFirstName: string;
|
||||
patientLastName: string;
|
||||
patientMobile?: string | null;
|
||||
providerUserId: string;
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
@@ -139,6 +140,8 @@ export interface PastTreatment {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isWalkIn: boolean;
|
||||
mobile?: string | null;
|
||||
email?: string | null;
|
||||
} | null;
|
||||
details: PastTreatmentDetail[];
|
||||
labCases: PastLabCase[];
|
||||
|
||||
Reference in New Issue
Block a user