feat(admin): name the tab for what it shows, prefill the patient, paginate and search

The tab was called 'نوبت‌های بعدی' but shows the whole course — finished sessions
with their recorded readings as much as upcoming ones. It is 'دوره‌های درمان' now.

Booking from a session still made the user search for a patient the page already
had open. The plan response carries the patient's national code (from the
profile, falling back to the user — the same COALESCE PatientController uses,
because users.national_code is routinely empty), and the modal takes a patient
prop that seeds the lookup and hides the search step. The old 'بیمار یافت شد'
card is suppressed in that mode; saying it twice is noise.

Sessions are now searchable and paged. A protocol allows up to 60 steps and a
patient can hold several courses, so an unbounded list was only ever going to
work for the small cases. Search filters on what the card actually shows —
service, staff, status, session number, area names — and runs in the page,
since /plan already returns the whole course and a round trip would add latency
and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 16:43:34 +03:30
co-authored by Claude Opus 5
parent 02fdfd13cb
commit 288919339e
5 changed files with 134 additions and 21 deletions
@@ -41,7 +41,7 @@ export interface BookingResource {
*/
export default function NewAppointmentModal({
slot, onClose, onSuccess, serviceMode = false, services = [], date, clinicUuid = null, resource = null,
treatmentSessionUuid = null,
treatmentSessionUuid = null, patient = null,
}: {
slot: BookingSlot;
onClose: () => void;
@@ -57,11 +57,20 @@ export default function NewAppointmentModal({
* اتصال از روی سرویس حدس زده می‌شود و سرویسِ اشتباه یک پروندهٔ موازی می‌سازد.
*/
treatmentSessionUuid?: string | null;
/**
* بیمارِ از پیش معلوم — وقتی مودال از پروندهٔ خودِ بیمار باز می‌شود.
*
* مرحلهٔ جستجو رد می‌شود: کسی که پروندهٔ بیمار را باز کرده نباید همان بیمار را
* دوباره با کد ملی پیدا کند.
*/
patient?: { name: string | null; mobile: string; national_code: string | null } | null;
}) {
const [mobile, setMobile] = useState('');
const [lookup, setLookup] = useState<PatientLookup | null>(null);
const [patientName, setPatientName] = useState('');
const [nationalCode, setNationalCode] = useState('');
const [mobile, setMobile] = useState(patient?.mobile ?? '');
const [lookup, setLookup] = useState<PatientLookup | null>(
patient === null ? null : { found: true, name: patient.name, mobile: patient.mobile, national_code: patient.national_code },
);
const [patientName, setPatientName] = useState(patient?.name ?? '');
const [nationalCode, setNationalCode] = useState(patient?.national_code ?? '');
// معیار جستجوی بیمار: کد ملی (پیش‌فرض) یا موبایل.
const [searchBy, setSearchBy] = useState<'mobile' | 'national'>('national');
const [pick, setPick] = useState<ServicePick>({ serviceUuids: [], durations: {}, slot: null });
@@ -329,7 +338,23 @@ export default function NewAppointmentModal({
)}
<Step n={pickerMode ? 2 : null} title="بیمار">
<div className="field-block" style={{ marginBottom: 14 }}>
{/* بیمارِ از پیش معلوم: فقط تأیید می‌شود، جستجو لازم نیست. */}
{patient !== null && (
<div style={{
marginBottom: 14, padding: '10px 14px', borderRadius: 'var(--r-sm)',
background: 'var(--success-bg)', display: 'flex', alignItems: 'center', gap: 10, fontSize: 13,
}}>
<CheckCircleIcon style={{ width: 20, height: 20, color: 'var(--success)', flexShrink: 0 }} />
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 700, color: 'var(--text)' }}>{patient.name || 'بدون نام'}</div>
<div style={{ color: 'var(--text-2)', fontSize: 12, direction: 'ltr', textAlign: 'start' }}>
{patient.mobile}{patient.national_code ? ` · ${patient.national_code}` : ''}
</div>
</div>
</div>
)}
<div className="field-block" style={{ marginBottom: 14, display: patient === null ? undefined : 'none' }}>
<label htmlFor="appt-patient-search">جستجوی بیمار <span className="req">*</span></label>
{/* انتخاب معیار جستجو: موبایل یا کد ملی */}
<div className="seg" style={{ display: 'flex', marginBottom: 8 }}>
@@ -391,7 +416,8 @@ export default function NewAppointmentModal({
</div>
</div>
{foundWithNationalCode && (
{/* وقتی بیمار از قبل معلوم است، کارت بالا همین را می‌گوید؛ دو بار گفتنش نویز است. */}
{patient === null && foundWithNationalCode && (
<div style={{
marginBottom: 16, padding: '10px 14px', borderRadius: 'var(--r-sm)',
background: 'var(--success-bg)', fontSize: 13,