feat(appointments): per-resource tabs backed by a resource_uuid list filter

Resources now get their own tabs on the appointments page, alongside doctors.
An appointment on "Laser CO2" belongs to the device, not to whichever doctor
happens to stand behind it, so selecting a resource tab replaces the doctor
filter instead of stacking on top of it.

GET /api/v1/my/appointments gains an optional resource_uuid filter and returns
a `resource` object per row. The join is a leftJoin on purpose: appointments
created before the resource-first model have no resource and must not drop out
of the list.

The resource tab lives in the URL so Back and refresh restore the same view,
per the list-state rule in CLAUDE.md. The doctor tab is still useState; moving
it is a separate refactor and was left untouched.

Verified against the running app: filtering by a resource returns only its
appointments, a resource from another tenant returns an empty list (TenantFilter,
200 not 403), and legacy rows still list with resource: null.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-03 11:31:37 +03:30
co-authored by Claude Opus 5
parent 8509a04ae2
commit 3e3a2482fc
6 changed files with 447 additions and 4 deletions
+34
View File
@@ -1085,6 +1085,40 @@ New query param `reserve=1` → returns only reserve-list entries; without it on
| `service_total_minutes` | int\|null | مدت ثبت‌شده؛ در حالت اسلاتی `null` |
| `service_buffer_minutes` | int\|null | بافر مؤثر لحظهٔ ثبت |
### فیلتر و فیلدِ منبع (2026-08)
صفحهٔ نوبت‌ها برای هر منبع تبِ مستقل دارد، پس فهرست باید بتواند «نوبت‌های همین دستگاه/اتاق»
را بدهد.
| پارامتر | نوع | توضیح |
|---|---|---|
| `resource_uuid` | string | اختیاری. فقط نوبت‌های همان منبع. با `doctor_uuid` جمع نمی‌شود — تبِ منبع جای تبِ پزشک را می‌گیرد، چون نوبتِ یک دستگاه می‌تواند از چند پزشک باشد |
| فیلد پاسخ | نوع | توضیح |
|---|---|---|
| `resource` | `{uuid, name}`\|null | منبعی که نوبت رویش گرفته شده. `null` برای نوبت‌های پیش از مدل منبع‌محور — با `leftJoin` گرفته می‌شود تا آن ردیف‌ها از فهرست حذف نشوند |
منبع تحت `TenantFilter` است: `resource_uuid`ِ محیط دیگر هیچ ردیفی برنمی‌گرداند (۲۰۰ با
فهرست خالی، نه ۴۰۳).
خروجی واقعی `GET /api/v1/my/appointments?limit=1&resource_uuid=ce070910-…`:
```json
{
"success": true,
"data": [
{
"uuid": "0e5268db-f2a6-4561-86e4-5c6de0270756",
"doctor_name": "امیر کاظمی",
"resource": { "uuid": "ce070910-7038-4f50-9f7a-1b35ec1a67f7", "name": "اتاق ۱" },
"slot_start": 1783859400,
"status": "completed"
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1, "limit": 1 }
}
```
`service_items` با یک کوئری جدا برای کل صفحه گرفته می‌شود، نه JOIN به کوئری اصلی: JOIN
روی collection ردیف‌ها را ضرب می‌کند و صفحه‌بندی را می‌شکند (نوبتی با سه سرویس، سه ردیف
می‌شد). N+1 هم نیست.