feat(resource): every resource is supervised by a doctor

Supervision now lives on the resource itself instead of being asked for again at
booking time, so one relation answers it everywhere.

The column is deliberately separate from the existing doctor_id bridge. That
bridge means "this resource IS this doctor" and isPerson() uses it to pin
capacity at 1; a supervised three-seat device must not become a person resource.
The FK is SET NULL rather than CASCADE because deleting a doctor should not take
the clinic's laser with it.

Required on create and non-clearable on update, enforced in the API where it can
give a Persian message. Ownership is checked through Clinic::hasDoctor so a
secretary cannot put their device under a doctor of another clinic; that returns
404, not 403, keeping foreign data invisible.

The 13 existing resources are backfilled deterministically: a practice resource
gets its own doctor, a clinic resource gets that clinic's first doctor. Both are
editable from the resource form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-03 12:15:42 +03:30
co-authored by Claude Opus 5
parent b03ae95bf8
commit ab4974d174
10 changed files with 318 additions and 10 deletions
+31
View File
@@ -178,6 +178,7 @@
|---|---|---|---|
| `address_uuid` | string | ✅ | محل نوبت‌دهی؛ جفت محیطِ منبع **از همین** مشتق می‌شود، نه از بدنه |
| `type_uuid` | string | ✅ | |
| `supervisor_doctor_uuid` | string | ✅ | پزشکِ ناظرِ منبع. باید پزشکِ همین محیط باشد وگرنه ۴۰۴ |
| `name` | string | ✅ | حداکثر ۱۵۰ نویسه |
| `capacity` | int | — | پیش‌فرض ۱، حداقل ۱؛ روی منبعِ شخص حداکثر ۱ |
| `setup_minutes` | int | — | پیش‌فرض ۰، بازهٔ ۰..۴۸۰ |
@@ -185,6 +186,36 @@
| `attributes` | object | — | حداکثر ۲۰ کلید · کلید `[a-z_]{1,40}` · مقدار فقط اسکالر |
| `active` | bool | — | پیش‌فرض `true` |
### پزشک ناظر (2026-08)
هر منبع **الزاماً** زیر نظر یک پزشک است؛ صفحهٔ نوبت‌ها تب منابع را زیر همان پزشک
می‌چیند، پس منبعِ بی‌ناظر جایی برای دیده‌شدن ندارد.
- `supervisor_doctor_uuid` در ساخت الزامی است → نبودش `422` با
`field: supervisor_doctor_uuid` و پیام «انتخاب پزشک ناظر الزامی است».
- در `PATCH` **اختیاری** است، ولی اگر بیاید نمی‌تواند خالی باشد (برداشتن ناظر ممنوع).
- پزشکِ خارج از محیط → `404` («پزشک ناظر یافت نشد») — وجود دادهٔ محیط بیگانه لو نمی‌رود.
- پاسخ‌ها فیلد `supervisor` را به شکل `{uuid, name}` برمی‌گردانند (یا `null` برای
ردیف‌هایی که پزشکشان حذف شده — کلید خارجی `SET NULL` است).
**ناظر با پلِ منبع فرق دارد.** `subject_kind`/`doctor_id` یعنی «این منبع خودِ همان
پزشک است» و `isPerson()` بر پایه‌اش ظرفیت را به ۱ قفل می‌کند. ناظرِ یک دستگاهِ
سه‌ظرفیتی نباید آن را به منبعِ انسانی تبدیل کند، پس ستون جداست.
خروجی واقعی `GET /api/v1/resources?active=1`:
```json
{
"name": "اتاق ۱",
"supervisor": { "uuid": "631e81d8-0009-4e01-a40f-3029905a3f27", "name": "امیر کاظمی" },
"subject_kind": null,
"capacity": 1
}
```
**backfill:** ۱۳ منبعِ موجود در migration ناظر گرفتند — منبعِ مطب → همان پزشک، منبعِ
کلینیک → پزشکِ اولِ همان کلینیک. قابل تغییر از فرم ویرایش منبع.
`attributes` عمداً آزاد است — کلید ناشناخته پذیرفته می‌شود — ولی مقدارش باید ساده
باشد. دلیل: تسک ۰۵ قید `same_gender` و تسک ۰۹ شرط‌های منبع را با مقایسهٔ ساده روی
همین مقادیر می‌سنجند؛ آرایهٔ تودرتو یعنی مقایسهٔ دلخواه، همان چیزی که بند ۸ مستند