# 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` را می‌گیرد. بدون این، هر سرویس موجود بی‌برنامه می‌شد.

**۲. بخش بدون هیچ نیازمندی معتبر است.** «انتظار در خانه» زمان می‌گیرد ولی هیچ منبعی
اشغال نمی‌کند.

**۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمی‌شود.** ۴۲۲ می‌دهد، چون رد
کردن بی‌صدا یعنی بیمار به منبعی می‌رسد که قرار نبود.

## سقف‌ها و ایمنی جایگزینی

| قید | مقدار | چرا |
|---|---|---|
| مجموع مدت | ۴۸۰ دقیقه | حفاظت از جستجوی وقت |
| تعداد بخش | ۲۰ | موتور برای هر بخش × هر نیازمندی ترکیب منابع را می‌سنجد |
| نیازمندی هر بخش | ۱۰ | همان |

`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=<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)
