feat(appointments): a resource-first view on the timeline

The appointments page only ever showed one doctor's row, but in the
resource-first model a single appointment can hold a room and a device at
the same time, and that — not the doctor's schedule — is what runs the
capacity out. An hour could look free on the doctor's lane while the only
alexandrite laser was already taken.

A third view, "منابع", draws one lane per resource for the selected day.
Blocks come from resource_occupancy rather than the appointment: that range
includes the device's setup and cleanup minutes and is the same range the
availability engine treats as busy. A multi-segment appointment therefore
shows up on every resource it holds, and each block links to the
appointment it belongs to.

GET /api/v1/resources/timeline keeps a fixed query count — one for
occupancy, one for shifts, one for the patient names — instead of one per
resource.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-02 14:14:10 +03:30
co-authored by Claude Opus 5
parent eeb9ae851a
commit 444ebc897a
9 changed files with 824 additions and 36 deletions
+18 -1
View File
@@ -23,6 +23,8 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import TurnsStatInfo from '../components/appointments/TurnsStatInfo';
import TurnsViewToggle from '../components/appointments/TurnsViewToggle';
import type { TurnsViewMode } from '../components/appointments/TurnsViewToggle';
import ResourceTimeline from '../components/appointments/ResourceTimeline';
import { useResourceTimeline } from '../hooks/useResourceTimeline';
import DoctorTabs from '../components/appointments/DoctorTabs';
import TurnsTimeline from '../components/appointments/TurnsTimeline';
import TurnsTable from '../components/appointments/TurnsTable';
@@ -616,6 +618,14 @@ export default function AppointmentsPage() {
const adminLocation = adminLocations.find(l => locKey(l) === adminLocKey) ?? adminLocations[0] ?? null;
const effectiveClinicUuid: string | null = isAdmin ? (adminLocation?.clinic_uuid ?? null) : clinicUuid;
// نمای منبع‌محور فقط وقتی درخواست می‌فرستد که انتخاب شده باشد؛ در نمای جدولی
// یک کوئری اضافه به‌ازای هر تغییر روز، بی‌مصرف است.
const {
day: resourceDay,
loading: resourceTimelineLoading,
error: resourceTimelineError,
} = useResourceTimeline(viewMode === 'resources' ? selectedDate : '');
// ── Slots query (timeline)
// effectiveClinicUuid باید در کلید کش باشد، وگرنه برنامهٔ یک محیط برای محیط دیگر نشان داده می‌شود.
const slotsQueryKey = ['appt-slots', selectedDoctorUuid, selectedDate, effectiveClinicUuid];
@@ -831,7 +841,14 @@ export default function AppointmentsPage() {
<DoctorTabs doctors={doctors} selected={selectedDoctorUuid} onSelect={setSelectedDoctorUuid} showAll={isAdmin} />
)}
<div style={{ padding: 16 }}>
{viewMode === 'table' ? (
{viewMode === 'resources' ? (
<ResourceTimeline
lanes={resourceDay?.resources ?? []}
dayStart={resourceDay?.date ?? 0}
loading={resourceTimelineLoading}
error={resourceTimelineError}
/>
) : viewMode === 'table' ? (
<>
<TurnsTable
items={pagedAppointments}