# تسک ۰۵ — بخش‌های نوبت و سازندهٔ برنامه

**فاز:** ۱ (هسته) · **وابستگی:** ۰۲، ۰۴ · **زمان:** ۱۶-۲۰ ساعت

---

## هدف

مهم‌ترین بخش مستند (بند ۷). یک نوبت یک تکه زمان پیوسته نیست:

| بخش | مدت | اتاق | اپراتور | دستگاه |
|---|---|---|---|---|
| مالیدن کرم بی‌حسی | ۵ | اشغال | اشغال | آزاد |
| انتظار اثر کرم | ۳۰ | اشغال | **آزاد** | آزاد |
| خود لیزر | ۲۰ | اشغال | اشغال | اشغال |
| مراقبت بعد | ۵ | اشغال | اشغال | آزاد |

با مدل امروز اپراتور ۶۰ دقیقه قفل می‌شود در حالی که ۳۰ دقیقه کار می‌کند. نصف ظرفیت
هدر می‌رود.

## وضعیت فعلی

```php
// src/Appointment/Entity/Appointment.php
private int $slotStart;   // یک بازهٔ پیوسته
private int $slotEnd;
```

هیچ مفهومی از بخش، و هیچ نیازمندی منبعی وجود ندارد. تنها منبعی که تداخلش بررسی می‌شود
پزشک است (`AppointmentRepository::isSlotTaken`).

## دامنه

**هست:**
- `SegmentTemplate` — الگوی بخش‌های یک سرویس (و بخش‌های اضافهٔ هر `ServiceOption`)
- `SegmentRequirement` — نیازمندی منبع هر بخش: نقش، تعداد، شرط مهارت، قید، نوع اشغال
- `AppointmentPlanBuilder` — از (سرویس، آیتم‌ها، بیمار، شعبه) یک **برنامهٔ نوبت** می‌سازد
- ادغام بخش‌های هم‌نوع، محاسبهٔ مدت هر بخش، چیدمان ترتیبی
- `GET /api/v1/appointment-plan/preview` برای دیدن برنامه پیش از جستجوی وقت

**نیست:** پیدا کردن منابع آزاد و زمان (تسک ۰۶)، ثبت اشغال (تسک ۰۷)،
اعمال قوانین روی برنامه (تسک ۰۹ — نقطهٔ اتصالش اینجا آماده می‌شود).

## Endpoint ها

| متد | مسیر | توضیح |
|---|---|---|
| GET/PUT | `/api/v1/service-item/{uuid}/segments` | الگوی بخش‌های سرویس |
| PUT | `/api/v1/segment-template/{uuid}/requirements` | نیازمندی‌های منبع یک بخش |
| POST | `/api/v1/appointment-plan/preview` | ساخت و برگرداندن برنامهٔ نوبت (بدون ثبت) |

## خروجی `POST /appointment-plan/preview`

```json
{
  "success": true,
  "data": {
    "total_minutes": 60,
    "segments": [
      { "sequence": 1, "name": "بی‌حسی موضعی", "offset_minutes": 0,  "duration_minutes": 5,
        "patient_present": true,
        "requirements": [
          { "role": "room",     "count": 1, "occupancy": "exclusive", "candidates": 3 },
          { "role": "operator", "count": 1, "occupancy": "exclusive", "candidates": 2 }
        ] },
      { "sequence": 2, "name": "انتظار", "offset_minutes": 5, "duration_minutes": 30,
        "patient_present": true, "mergeable": true,
        "requirements": [ { "role": "room", "count": 1, "occupancy": "exclusive", "candidates": 3 } ] }
    ]
  }
}
```

`candidates` تعداد منابع واجد شرایط است — اگر صفر باشد، برنامه ساخته نمی‌شود و خطای
انسانی برمی‌گردد: «هیچ اپراتور خانمی با مهارت لیزر در این شعبه نیست» (مستند بند ۱۰).

## معیار پذیرش

- ✅ موفق: سرویس «لیزر» با چهار بخش بالا تعریف می‌شود؛ `preview` برنامه‌ای با
  `total_minutes = 60` و چهار بخش با `offset_minutes` صحیح (۰، ۵، ۳۵، ۵۵) برمی‌گرداند.
- ✅ موفق: انتخاب دو ناحیه (صورت + بیکینی) → بخش «آماده‌سازی» **یک بار** می‌آید
  (`mergeable=true` هم‌نوع‌ها ادغام می‌شوند) ولی بخش «لیزر» مدتش با `DurationCalculator`
  تسک ۰۴ محاسبه شده است.
- ✅ موفق: سرویسی که هیچ `SegmentTemplate` ندارد → برنامه‌ای با **یک بخش** برابر کل مدت
  و نیازمندی پیش‌فرض (منبع `type=doctor`). این همان رفتار امروز است.
- ❌ خطا: نیازمندی با مهارتی که هیچ منبعی در آن شعبه ندارد →
  `422` با `ERR_NO_ELIGIBLE_RESOURCE` و پیام فارسی شامل نقش و مهارت.
- ❌ خطا: بخش با `duration_minutes <= 0` و بدون منبع مدت پویا → `422`.
- ⚠️ مرزی: بخش با `requirements = []` (مثلاً «انتظار در خانه») → معتبر؛ زمان می‌گیرد،
  هیچ منبعی نمی‌گیرد.
- ⚠️ مرزی: قید `same_gender_as_patient` وقتی جنسیت بیمار نامشخص است → نیازمندی نادیده
  گرفته **نمی‌شود**؛ `422` با پیام «برای این سرویس ثبت جنسیت بیمار الزامی است».
- ⚠️ مرزی: `setup/cleanup` منبع در `preview` **نمایش داده نمی‌شود** ولی در
  `occupancy_offset` هر نیازمندی می‌آید تا تسک ۰۶ همان را استفاده کند.
- ⚠️ مرزی: مجموع مدت بخش‌ها بیشتر از ۸ ساعت → `422` (حفاظت از جستجوی وقت).

## خروجی

- `src/Appointment/Plan/` — entity ها، `AppointmentPlanBuilder`، DTO ها
- `assets/admin/pages/ServiceSegmentsPage.tsx` + پیش‌نمایش برنامه
- `docs/api/appointment-plan.md`
- الگوهای آماده: `app:segment:seed-templates --preset=beauty|dental|physio` (مستند بند ۱۷)
