refactor(admin): course cards with a detail view, and fill the banner's next date

The tab stacked every course's full session list on one page. A protocol allows
sixty steps, so one open course was enough to bury the others. Courses are cards
now — service, progress, supervising doctor, staff — and opening one replaces
the list with its detail: a back button, two tabs (the whole course, or only
what is still to come), search and paging inside each. The choice lives in the
URL so browser-back returns to the same course.

Booking stays where the work is: the button sits on the session card inside the
upcoming tab, not on a separate page.

The patient banner's 'نوبت بعدی' read '—' for anyone mid-course, because it only
looked at booked appointments and a course's later sessions have none yet. It
now falls back to the next session of the active course and relabels itself
'جلسهٔ بعدی' when it does — a planned session is not a booking, and the banner
should not call it one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 17:04:55 +03:30
co-authored by Claude Opus 5
parent 8672608696
commit f61b79b67e
3 changed files with 254 additions and 138 deletions
+17 -2
View File
@@ -44,13 +44,20 @@ const InfoLine = ({ icon, label, value }: { icon: React.ReactNode; label: string
* FileServicesHeader (name + status chip, file number, tags, contact/date,
* next appointment, یادداشت button).
*/
export default function PatientCaseBanner({ name, recordNumber, mobile, createdAt, tags, nextAppointment, hasDebt, noShows, onAddNote }: {
export default function PatientCaseBanner({ name, recordNumber, mobile, createdAt, tags, nextAppointment, nextSession, hasDebt, noShows, onAddNote }: {
name: string;
recordNumber?: string | null;
mobile?: string | null;
createdAt?: number;
tags?: Tag[];
nextAppointment?: number | null;
/**
* جلسهٔ بعدیِ دوره وقتی نوبتی رزرو نشده.
*
* جدا از `nextAppointment` است و باید هم باشد: این هنوز نوبت نیست، برنامه است.
* یکی کردنشان یعنی بنر چیزی را «نوبت» بنامد که کسی رزروش نکرده.
*/
nextSession?: number | null;
hasDebt?: boolean;
/** خلاصهٔ عدم حضور در پنجرهٔ سیاست — `null` یعنی هنوز نیامده. */
noShows?: { count: number; threshold: number; window_days: number; at_risk: boolean } | null;
@@ -106,7 +113,15 @@ export default function PatientCaseBanner({ name, recordNumber, mobile, createdA
{/* left — next appointment + note */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 28 }}>
<InfoLine icon={<FilesServiceNotification />} label="نوبت بعدی:" value={nextAppointment ? formatDate(nextAppointment) : '—'} />
<InfoLine
icon={<FilesServiceNotification />}
label={nextAppointment || !nextSession ? 'نوبت بعدی:' : 'جلسهٔ بعدی:'}
value={
nextAppointment ? formatDate(nextAppointment)
: nextSession ? formatDate(nextSession)
: '—'
}
/>
<button
type="button" onClick={onAddNote}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: 'var(--accent)', color: 'var(--on-primary)', border: 'none', borderRadius: 10, padding: '0 18px', height: 36, fontSize: 13, fontWeight: 600, cursor: 'pointer' }}