Two reported bugs, one root cause: resource tabs were built as a rival selection to the doctor rather than a narrower view within them. Selecting a resource cleared the doctor. The auto-select effect then quietly put the *first* doctor back, so anyone working under the second doctor was thrown to the first and lost that doctor's own booking. Selecting a resource now leaves the doctor alone; only picking a doctor clears the resource. A resource tab also still rendered the doctor's slot timeline, just with no data. Resources have no slotted weekly schedule — their calendar comes from service duration and real occupancy — so showing a slot grid promises times the booking engine does not recognise. The resource tab now renders its own panel: that day's appointments on the resource plus a service booking entry point. Both regressions are pinned by tests, and both were checked by reverting each fix in turn. The first attempt at the doctor-retention test passed even with the bug restored, because the auto-select effect masked it; it was rewritten to use the second doctor, where the bounce is observable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
import React from 'react';
|
||
import { PlusIcon } from '@heroicons/react/24/outline';
|
||
import { formatNumber } from '../../lib/utils';
|
||
import StatusBadge from '../ui/StatusBadge';
|
||
import type { Appointment, ClinicResource } from '../../types';
|
||
|
||
function hhmm(ts: number): string {
|
||
const d = new Date(ts * 1000);
|
||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||
}
|
||
|
||
/**
|
||
* نمای روزِ یک منبع.
|
||
*
|
||
* عمداً تایملاین اسلاتی نیست: منبع برنامهٔ هفتگیِ اسلاتشده ندارد، تقویمش از مدتِ
|
||
* سرویسها و اشغالِ واقعی ساخته میشود. نشان دادن شبکهٔ اسلات برای منبع یعنی وعدهٔ
|
||
* زمانهایی که موتور رزرو اصلاً نمیشناسد.
|
||
*
|
||
* پس فقط دو چیز: آنچه امروز روی این منبع رزرو شده، و یک راه برای افزودن نوبتِ سرویسی.
|
||
*/
|
||
export default function ResourceDayPanel({
|
||
resource, appointments, loading, canCreate, onBook, onView,
|
||
}: {
|
||
resource: ClinicResource;
|
||
appointments: Appointment[];
|
||
loading: boolean;
|
||
canCreate: boolean;
|
||
onBook: () => void;
|
||
onView: (appointment: Appointment) => void;
|
||
}) {
|
||
const rows = [...appointments].sort((a, b) => a.slot_start - b.slot_start);
|
||
|
||
return (
|
||
<div>
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8,
|
||
padding: '8px 12px', marginBottom: 12, borderRadius: 'var(--r-sm)',
|
||
background: 'var(--surface-2)', border: '1px solid var(--border)',
|
||
fontSize: 12.5, color: 'var(--text-2)',
|
||
}}>
|
||
<span>
|
||
نوبتدهی این منبع سرویسی است — زمان از مدت سرویس و آزادبودن خودِ منبع میآید، نه از اسلات ثابت.
|
||
</span>
|
||
{resource.supervisor && (
|
||
<span style={{ color: 'var(--text-3)', flexShrink: 0 }}>
|
||
زیر نظر {resource.supervisor.name}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{canCreate && (
|
||
<button
|
||
type="button"
|
||
className="btn primary"
|
||
onClick={onBook}
|
||
style={{ marginBottom: 14 }}
|
||
>
|
||
<PlusIcon style={{ width: 16 }} /> افزودن نوبت سرویس
|
||
</button>
|
||
)}
|
||
|
||
{loading ? (
|
||
<div className="skeleton" style={{ height: 90, borderRadius: 'var(--r-sm)' }} />
|
||
) : rows.length === 0 ? (
|
||
<p style={{ fontSize: 13, color: 'var(--text-3)', margin: 0, lineHeight: 2 }}>
|
||
برای «{resource.name}» در این روز نوبتی ثبت نشده است.
|
||
</p>
|
||
) : (
|
||
<div style={{ display: 'grid', gap: 8 }}>
|
||
{rows.map((a) => (
|
||
<button
|
||
key={a.uuid}
|
||
type="button"
|
||
onClick={() => onView(a)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 12, width: '100%', textAlign: 'right',
|
||
padding: '10px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
|
||
border: '1px solid var(--border)', background: 'var(--surface)', fontFamily: 'inherit',
|
||
}}
|
||
>
|
||
<span dir="ltr" style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', flexShrink: 0 }}>
|
||
{hhmm(a.slot_start)} – {hhmm(a.slot_end)}
|
||
</span>
|
||
<span style={{ flex: 1, minWidth: 0, fontSize: 13, color: 'var(--text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{a.patient_name || '—'}
|
||
{a.service_item?.name ? ` · ${a.service_item.name}` : ''}
|
||
</span>
|
||
<StatusBadge type="appointment" value={a.status} />
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{rows.length > 0 && (
|
||
<p className="field-hint">{formatNumber(rows.length)} نوبت روی این منبع در این روز</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|