Writing the query-count test that task 14 owed showed the growth was real: one resource cost 10 queries, six cost 33 — about five per resource, because the available-minutes figure walked each resource's calendar on its own. Holidays, tenant overrides and branch hours are identical for every resource in a report, so they now load once outside the loop; shifts and exceptions load for all resources in one query each. The batched path is a new method rather than a change to rawAvailability, which the booking engine also calls. The test pins the shape of the growth, not an exact count. Also landed: - app:segment:seed-templates with beauty, dental and physio presets. Building four segments and their requirements by hand is the first thing a new clinic must do and the most tedious; this gives them something to edit instead of an empty page. It refuses to touch a service that already has segments unless --force, and it will not invent resource types the tenant never defined. - book-all is all-or-nothing, proven rather than asserted: with a calendar open one day a week and a 1-2 day protocol gap, session one finds a slot and session two cannot, and every session must come back planned. - credit_refundable: false takes the credit back with a negative adjustment and deletes nothing — the ledger stays append-only. - the segments editor has frontend tests, including that it sends back what the user sees and renders read-only without the permission. useBranches now returns [] for a non-array payload instead of throwing "branches.map is not a function" and taking the page down with it. BookingLocationsScanTest built a Clinic around a Doctor loaded from a different manager, which Doctrine treats as a new entity; it flushed fine most runs and failed on cascade in others. It now loads the doctor from the same manager. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
183 lines
9.2 KiB
Markdown
183 lines
9.2 KiB
Markdown
# Appointment Plan API — بخشهای نوبت و سازندهٔ برنامه
|
||
|
||
> **Base:** `/api/v1` · **Auth:** JWT
|
||
> وابسته به [resource.md](resource.md) (منبع و مهارت) و [clinic-services.md](clinic-services.md) (مدت آیتمها).
|
||
|
||
---
|
||
|
||
## چرا نوبت یک تکه نیست
|
||
|
||
بند ۷ مستند. یک جلسهٔ لیزر:
|
||
|
||
| بخش | مدت | اتاق | اپراتور | دستگاه |
|
||
|---|---|---|---|---|
|
||
| مالیدن کرم بیحسی | ۵ | اشغال | اشغال | آزاد |
|
||
| انتظار اثر کرم | ۳۰ | اشغال | **آزاد** | آزاد |
|
||
| خود لیزر | ۲۰ | اشغال | اشغال | اشغال |
|
||
| مراقبت بعد | ۵ | اشغال | اشغال | آزاد |
|
||
|
||
با مدل تکبازهای، اپراتور ۶۰ دقیقه قفل میشود در حالی که ۳۰ دقیقه کار میکند —
|
||
**نصف ظرفیت هدر میرود**.
|
||
|
||
⚠️ این بخش هیچ زمان مطلقی و هیچ منبع مشخصی تعیین نمیکند. فقط **شکل** نوبت را میسازد؛
|
||
پیدا کردن وقت و منبع آزاد کارِ تسک بعدی است.
|
||
|
||
---
|
||
|
||
## `GET/PUT /api/v1/service-item/{uuid}/segments`
|
||
|
||
`PUT` جایگزینی کامل است. هر بخش:
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|---|---|---|
|
||
| `sequence` | int | ترتیب اجرا |
|
||
| `name` | string | ✅ الزامی |
|
||
| `duration_source` | `fixed` \| `items` | پیشفرض `fixed` |
|
||
| `duration_minutes` | int | برای `fixed` باید مثبت باشد |
|
||
| `patient_present` | bool | پیشفرض `true`؛ «انتظار در خانه» خلافش است |
|
||
| `mergeable` | bool | با چند آیتم **یک بار** میآید |
|
||
| `requirements` | array | نیازمندی منبع |
|
||
|
||
**`duration_source: "items"`** یعنی مدت این بخش از آیتمهای انتخابشده میآید و با
|
||
فرمول تسک ۰۴ حساب میشود. «خود لیزر» با دو ناحیه طولانیتر میشود، ولی «انتظار اثر
|
||
کرم» نه — به همین دلیل دو منبع مدت لازم است و یک عدد ثابت کافی نیست.
|
||
|
||
هر نیازمندی:
|
||
|
||
| فیلد | توضیح |
|
||
|---|---|
|
||
| `type_uuid` | ✅ نوع منبع (اتاق، اپراتور، دستگاه) |
|
||
| `skill_uuid` | مهارت لازم |
|
||
| `count` | پیشفرض ۱ |
|
||
| `occupancy` | `exclusive` (قفل کامل) یا `shared` (ظرفیت میشمارد) |
|
||
| `constraints` | فعلاً فقط `same_gender_as_patient` |
|
||
|
||
**۴۲۲:** نام خالی · بخش `fixed` با مدت صفر · مجموع بیش از ۴۸۰ دقیقه · `occupancy` یا
|
||
`constraint` ناشناخته.
|
||
|
||
---
|
||
|
||
## `POST /api/v1/appointment-plan/preview`
|
||
|
||
```json
|
||
{
|
||
"service_uuid": "…لیزر",
|
||
"branch_uuid": "…شعبه",
|
||
"item_uuids": ["…صورت", "…بیکینی"],
|
||
"patient_gender": "female"
|
||
}
|
||
```
|
||
|
||
**۲۰۰:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"total_minutes": 60,
|
||
"segments": [
|
||
{ "sequence": 1, "name": "بیحسی موضعی", "offset_minutes": 0, "duration_minutes": 5,
|
||
"patient_present": true, "mergeable": false,
|
||
"requirements": [
|
||
{ "role": "room", "role_name": "اتاق", "skill_name": null, "count": 1,
|
||
"occupancy": "exclusive", "constraints": [], "candidates": 3,
|
||
"occupancy_offset": { "setup_minutes": 0, "cleanup_minutes": 0 } }
|
||
] }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
`candidates` تعداد منابع واجد شرایط است. `occupancy_offset` در نمای کاربر نمایش داده
|
||
**نمیشود**؛ برای موتور جستجوی وقت است و محافظهکارانه از بیشترین مقدارِ کاندیدها
|
||
گرفته میشود — کم گرفتنش یعنی نوبت بعدی روی زمان تمیزکاری بیفتد.
|
||
|
||
### خطاها
|
||
|
||
| کد | کِی |
|
||
|---|---|
|
||
| `ERR_NO_ELIGIBLE_RESOURCE` (۴۲۲) | هیچ منبعی شرایط یک بخش را ندارد |
|
||
| `ERR_VALIDATION_002` (۴۲۲) | قید جنسیت هست ولی جنسیت بیمار نامشخص است |
|
||
| `ERR_VALIDATION_001` (۴۲۲) | مدت بخش تعیین نشده · مجموع بیش از سقف |
|
||
| ۴۰۴ | سرویس یا شعبهٔ محیط دیگر |
|
||
|
||
پیام `ERR_NO_ELIGIBLE_RESOURCE` انسانی است و نقش، مهارت و شعبه را میگوید:
|
||
«هیچ اپراتور خانمی با مهارت «لیزر آلکساندرایت» در شعبهٔ «مرکزی» موجود نیست» (بند ۱۰).
|
||
|
||
---
|
||
|
||
## سه قرارداد
|
||
|
||
**۱. سرویس بدون الگوی بخش، همان رفتار امروز را میگیرد.** یک بخش پیوسته به اندازهٔ کل
|
||
مدت که منبعِ `type=doctor` را میگیرد. بدون این، هر سرویس موجود بیبرنامه میشد.
|
||
|
||
**۲. بخش بدون هیچ نیازمندی معتبر است.** «انتظار در خانه» زمان میگیرد ولی هیچ منبعی
|
||
اشغال نمیکند.
|
||
|
||
**۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمیشود.** ۴۲۲ میدهد، چون رد
|
||
کردن بیصدا یعنی بیمار به منبعی میرسد که قرار نبود.
|
||
|
||
## ادغام بخشها
|
||
|
||
برنامه از الگوهای **سرویس اصلی بهعلاوهٔ آیتمهای انتخابشده** ساخته میشود. تا پیش از
|
||
این فقط الگوهای سرویس اصلی خوانده میشد، پس بخشهایی که کلینیک روی خودِ ناحیه تعریف کرده
|
||
بود بیصدا نادیده میماند و `mergeable` هرگز کاری نمیکرد — در یک سرویس، دو بخشِ همنام
|
||
معنا ندارد.
|
||
|
||
| قاعده | چرا |
|
||
|---|---|
|
||
| همنامهای `mergeable` یک بار میآیند | «آمادهسازی» برای دو ناحیه یک بار انجام میشود |
|
||
| از میان همنامها **طولانیترین** میماند | آمادهسازی دو ناحیه کوتاهتر از طولانیترینشان نیست |
|
||
| `duration_source: "items"` هم یک بار میآید | `DurationCalculator` از قبل مجموع همهٔ آیتمها را داده؛ تکرارش یعنی دوبار شمردن |
|
||
| تعداد منبع پس از ادغام **بیشینه** است | دو ناحیه با هم دو اتاق نمیخواهند، ولی اگر یکی دو اپراتور لازم داشت ادغام نباید به یک تنزلش بدهد |
|
||
|
||
مثال: «لیزر صورت» با آمادهسازی ۵ دقیقه و «لیزر بیکینی» با آمادهسازی ۱۲ دقیقه، هر دو
|
||
انتخاب شوند → یک آمادهسازیِ ۱۲ دقیقهای، و کارِ اصلی به اندازهٔ مجموع دو ناحیه.
|
||
|
||
**برنامه قطعی است:** دو `preview` با همان ورودی خروجیِ بایتبهبایت یکسان میدهند (تست
|
||
دارد). برنامهای که بین پیشنمایش و رزرو جابهجا شود یعنی کاربر چیزی را تأیید کرده که
|
||
رزرو نشد.
|
||
|
||
---
|
||
|
||
## طبقهبندی محیط
|
||
|
||
| جدول | وضعیت |
|
||
|---|---|
|
||
| `segment_templates` | جفت محیط (از بخشِ سرویس مشتق میشود) |
|
||
| `segment_requirements` | `AGGREGATE_CHILDREN` — ریشه `SegmentTemplate` |
|
||
|
||
## الگوی نمونه
|
||
|
||
```bash
|
||
ddev exec php bin/console app:segment:seed-templates --service=<uuid> --preset=beauty
|
||
```
|
||
|
||
سه الگو: `beauty` (آمادهسازی · بیحسی · انتظار · کار اصلی · تمیزکاری) · `dental`
|
||
(معاینه · درمان · ضدعفونی یونیت) · `physio` (ارزیابی · جلسهٔ درمان · استراحت).
|
||
|
||
نقطهٔ شروع است نه پیکربندی نهایی: کلینیک از روی چیزی که میبیند ویرایش میکند، نه از روی
|
||
صفحهٔ خالی. روی سرویسی که از قبل بخش دارد **کاری نمیکند** مگر `--force` — بازنویسی
|
||
خاموشِ چیزی که کلینیک خودش ساخته، بدترین رفتار ممکن است. نقشی که آن محیط تعریف نکرده،
|
||
ساخته نمیشود و در خروجی گزارش میشود؛ نوع منبع تصمیم کلینیک است.
|
||
|
||
## تستها
|
||
|
||
```bash
|
||
ddev exec php bin/phpunit tests/Appointment/AppointmentPlanTest.php # ۱۵ تست
|
||
```
|
||
|
||
---
|
||
|
||
## اثر موتور قوانین
|
||
|
||
- دستهٔ `timing`: `min_duration_minutes` (بیشترین برنده) و `add_duration_minutes` (جمع)
|
||
روی **مجموع** نوبت اعمال میشوند؛ رشدِ لازم به **آخرین** بخش میچسبد تا آفست بخشهای
|
||
قبلی جابهجا نشود.
|
||
- دستهٔ `resource`: نقشی که `require_resource` میخواهد، اگر هیچ بخشی نداشته باشد، به
|
||
**اولین بخشی که بیمار حاضر است** اضافه میشود. نقش ناشناخته یا بیمنبع `422` میدهد، نه
|
||
بیاثر ماندن.
|
||
- هر دو روی سرویسِ **بیالگو** هم اجرا میشوند.
|
||
|
||
جزئیات: [policy.md](policy.md)
|