# Appointment Plan API — بخش‌های نوبت و سازندهٔ برنامه > **Base:** `/api/v1` · **Auth:** JWT > وابسته به [resource.md](resource.md) (منبع و مهارت) و [clinic-services.md](clinic-services.md) (مدت آیتم‌ها). --- ## چرا نوبت یک تکه نیست بند ۷ مستند. یک جلسهٔ لیزر: | بخش | مدت | اتاق | اپراتور | دستگاه | |---|---|---|---|---| | مالیدن کرم بی‌حسی | ۵ | اشغال | اشغال | آزاد | | انتظار اثر کرم | ۳۰ | اشغال | **آزاد** | آزاد | | خود لیزر | ۲۰ | اشغال | اشغال | اشغال | | مراقبت بعد | ۵ | اشغال | اشغال | آزاد | با مدل تک‌بازه‌ای، اپراتور ۶۰ دقیقه قفل می‌شود در حالی که ۳۰ دقیقه کار می‌کند — **نصف ظرفیت هدر می‌رود**. ⚠️ این بخش هیچ زمان مطلقی و هیچ منبع مشخصی تعیین نمی‌کند. فقط **شکل** نوبت را می‌سازد؛ پیدا کردن وقت و منبع آزاد کارِ تسک بعدی است. --- ## مجوزها از ۲۰۲۶-۰۸-۰۸: - `GET /service-item/{uuid}/segments` → `services.view` - `PUT /service-item/{uuid}/segments` → `services.update` - `POST /appointment-plan/preview` → `services.view` **یا** `appointments.view` منبعش `services` است نه `appointments`: بخش‌بندی یک خاصیتِ `ServiceItem` است و صفحه‌اش داخل کاتالوگ خدمات می‌نشیند. همان استدلالِ پروتکل درمان در آدیت ۲۰۲۶-۰۸-۰۷. `preview` استثناست و «یا» می‌گیرد، چون ورودیِ فرمِ ثبت نوبت است نه پیکربندیِ سرویس: منشی‌ای که اجازهٔ ثبت نوبت دارد ولی کاتالوگ خدمات برایش بسته است، وگرنه نمی‌توانست همان نوبتی را که مجاز است ثبت کند. قرینهٔ `ResourcePermissionTrait::denyUnlessGrantedForBooking`. > **چرا اضافه شد:** این کنترلر دقیقاً همان شکلِ `TreatmentProtocolController` پیش از > رفعِ یافتهٔ ۱ آدیت را داشت — `#[IsGranted('IS_AUTHENTICATED_FULLY')]` سطح‌کلاس و یک > `requireItem()` که فقط مالکیتِ محیط را می‌سنجد. مالکیت مجوز نیست: عبور از آن فقط > ثابت می‌کند سرویس مالِ همین محیط است، نه اینکه این کاربر حق دست‌زدن به آن را دارد. --- ## `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` را می‌گیرد. بدون این، هر سرویس موجود بی‌برنامه می‌شد. **۲. بخش بدون هیچ نیازمندی معتبر است.** «انتظار در خانه» زمان می‌گیرد ولی هیچ منبعی اشغال نمی‌کند. **۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمی‌شود.** ۴۲۲ می‌دهد، چون رد کردن بی‌صدا یعنی بیمار به منبعی می‌رسد که قرار نبود. ## سقف‌ها و ایمنی جایگزینی | قید | مقدار | چرا | |---|---|---| | مجموع مدت | ۴۸۰ دقیقه | حفاظت از جستجوی وقت | | تعداد بخش | ۲۰ | موتور برای هر بخش × هر نیازمندی ترکیب منابع را می‌سنجد | | نیازمندی هر بخش | ۱۰ | همان | `PUT .../segments` **حذف‌کن-و-بنویس** است. همهٔ اعتبارسنجی‌ها (قید ناشناخته، نوع اشغال، مدت، سقف‌ها) **پیش از حذف** انجام می‌شوند و خودِ حذف و نوشتن در یک تراکنش‌اند: خطای بعد از حذف یعنی سرویس بدون بخش می‌ماند و نوبت‌دهی‌اش بی‌صدا به «یک بخش پیوسته» برمی‌گردد — که مدت و منابع همهٔ نوبت‌های بعدی را عوض می‌کند. پاسخ `preview` علاوه بر `total_minutes`، فیلد `patient_facing_minutes` هم دارد: مدتی که بیمار واقعاً روی صندلی است. نوبت نودقیقه‌ای که چهل دقیقه‌اش انتظار اثر بی‌حسی است، «نود دقیقه وقت بگذارید» نیست — و محاسبه یک‌جا در بک‌اند است تا هر کلاینت خودش جمع نزند. ## ادغام بخش‌ها برنامه از الگوهای **سرویس اصلی به‌علاوهٔ آیتم‌های انتخاب‌شده** ساخته می‌شود. تا پیش از این فقط الگوهای سرویس اصلی خوانده می‌شد، پس بخش‌هایی که کلینیک روی خودِ ناحیه تعریف کرده بود بی‌صدا نادیده می‌ماند و `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= --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)