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
+53
View File
@@ -335,6 +335,59 @@
> **اتمی است.** اعتبارسنجی کل فهرست پیش از هر حذفی انجام می‌شود، پس یک ردیف نامعتبر
> در انتهای فهرست، مهارت‌های درستِ قبلی را پاک نمی‌کند و بعد ۴۲۲ برگرداند.
### `GET /api/v1/resources/timeline`
مجوز: `appointment_settings.view`. پشتِ نمای **منابع** در صفحهٔ نوبت‌ها
(`/admin/appointments`) است.
| پارامتر | پیش‌فرض | توضیح |
|---|---|---|
| `date` | امروز | `YYYY-MM-DD` — هر قالب دیگری ۴۲۲ |
| `address_uuid` | همهٔ شعبه‌ها | فقط منابع همان شعبه |
فقط منابع **فعال** برمی‌گردند و منبعِ بی‌شیفت هم در فهرست می‌ماند تا ردیفش در تایم‌لاین
دیده شود.
بازه‌ها از `resource_occupancy` می‌آیند نه از خودِ نوبت: بازهٔ اشغال، آماده‌سازی و
تمیزکاری منبع را هم در بر دارد و همان بازه‌ای است که موتور جستجو اشغال می‌بیند. ردیف
`released` نمی‌آید؛ آن تاریخچه است. یک نوبتِ چندبخشی روی چند منبع، چند ردیف دارد —
همان چیزی که نمای پزشک‌محور نشان نمی‌دهد.
خروجی واقعی (سناریوی ۲، ۲۰۲۶-۰۸-۰۵):
```jsonc
{
"success": true,
"data": {
"date": 1785875400, // نیمه‌شب همان روز
"day_of_week": 4, // ۰ = شنبه
"resources": [
{
"uuid": "…", "name": "اتاق لیزر ۱", "type_name": "اتاق درمان",
"address_name": "درمانگاه سلامت", "capacity": 1,
"shifts": [{ "start_minute": 480, "end_minute": 1260 }],
"items": [
{
"uuid": "6176e73b-df27-4cdf-815c-36f9bfbd68ca",
"starts_at": 1785931200, "ends_at": 1785931500,
"status": "booked", "segment_name": "بی‌حسی موضعی",
"appointment_id": 47, "patient_name": "زهرا احمدی",
"appointment_uuid": "17086c41-6cc4-4539-bb5b-4c94e8f373c6",
"appointment_status": "pending"
}
]
}
]
}
}
```
`shifts` فقط شیفت‌های همان روزِ هفته است. **۴۲۲:** قالب `date` غلط
(`{"code":"ERR_VALIDATION_002","message":"تاریخ باید به شکل YYYY-MM-DD باشد","field":"date"}`).
> تعداد کوئری ثابت است: یک کوئری اشغال، یک کوئری شیفت، یک کوئری نوبت — نه یکی به‌ازای
> هر منبع.
### `PUT /api/v1/resource/{uuid}/categories`
مجوز: `appointment_settings.update`.