The mergeable flag was stored, returned by the API and rendered in the editor while changing nothing. The reason was upstream: the builder only ever read the primary service's templates, and within one service two segments with the same name do not occur — so the dedupe it already had could never fire. Templates now come from the primary service plus every selected item, and same-named mergeable segments collapse to one. Rules, with their reasons: - the longest of the same-named segments survives — prepping two areas is not shorter than prepping the longer one alone - a duration_source: "items" segment also appears once even when it is not marked mergeable, because DurationCalculator has already summed every item and repeating the segment counts that time twice - the merged requirement count is the maximum, not the sum and not the first one seen: two areas do not need two rooms, but if one of them needed two operators, merging must not quietly demote that to one Also pins that the plan is deterministic: two previews of the same input are compared byte for byte. A plan that shifts between preview and booking means the user confirmed something that was not what got booked. Unrelated but found by running the suite on a Saturday: testPastStartsAreExcluded searched "last week's Saturday", which is today when today is Saturday, so this afternoon's slots were legitimately not in the past. It now searches two weeks back, which is unambiguous on every weekday. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
169 lines
8.2 KiB
Markdown
169 lines
8.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/phpunit tests/Appointment/AppointmentPlanTest.php # ۱۴ تست
|
||
```
|
||
|
||
---
|
||
|
||
## اثر موتور قوانین
|
||
|
||
- دستهٔ `timing`: `min_duration_minutes` (بیشترین برنده) و `add_duration_minutes` (جمع)
|
||
روی **مجموع** نوبت اعمال میشوند؛ رشدِ لازم به **آخرین** بخش میچسبد تا آفست بخشهای
|
||
قبلی جابهجا نشود.
|
||
- دستهٔ `resource`: نقشی که `require_resource` میخواهد، اگر هیچ بخشی نداشته باشد، به
|
||
**اولین بخشی که بیمار حاضر است** اضافه میشود. نقش ناشناخته یا بیمنبع `422` میدهد، نه
|
||
بیاثر ماندن.
|
||
- هر دو روی سرویسِ **بیالگو** هم اجرا میشوند.
|
||
|
||
جزئیات: [policy.md](policy.md)
|