# Appointment Availability API — جستجوی وقت چندمنبعی

> **Base:** `/api/v1` · **Auth:** JWT
> وابسته به [appointment-plan.md](appointment-plan.md) و [resource-calendar.md](resource-calendar.md).

---

## چه چیزی را حل می‌کند

بند ۱۰ مستند: برنامهٔ چندبخشی نوبت را روی تقویم منابع بلغزان و بگو چه ساعت‌هایی
**واقعاً** ممکن‌اند، با پیشنهاد اینکه کدام منبع استفاده شود.

مسیر قبلی فقط تداخل **پزشک** را می‌سنجید؛ اتاق، دستگاه و اپراتور اصلاً وجود نداشتند.

### چرا این ظرفیت آزاد می‌کند

تخصیص **per نقش** است، نه per بخش. اپراتوری که در بخش «انتظار اثر کرم» نیازمندی
ندارد، در آن دقایق بررسی نمی‌شود و برای بیمار دیگری آزاد است.

نمونهٔ عینی (و تستِ مرجعِ این تسک): بیمار الف ۱۰:۰۰–۱۱:۰۰ نوبت دارد ولی اپراتور فقط
۱۰:۰۰–۱۰:۰۵ و ۱۰:۳۵–۱۱:۰۰ درگیر است. اگر اتاق دومی آزاد باشد، بیمار ب در بازهٔ
۱۰:۰۵–۱۰:۳۵ جا می‌شود. با مدل تک‌بازه‌ای، آن نیم‌ساعت هدر می‌رفت.

### چرا همان منبع در بخش‌های غیرمجاور

یک منبع برای **همهٔ** بخش‌هایی که آن نقش را می‌خواهند انتخاب می‌شود. اپراتور بخش ۱ و
بخش ۳ باید یک نفر باشد؛ انتخاب مستقل per بخش، دو نفر می‌داد و بیمار وسط کار تحویل
شخص دیگری می‌شد.

---

## `POST /api/v1/appointment-availability`

```json
{
  "service_uuid": "…",
  "branch_uuid": "…",
  "from": 1785529800,
  "to": 1785616200,
  "item_uuids": ["…"],
  "patient_gender": "female",
  "doctor_uuid": "…",
  "step_minutes": 15
}
```

`from`/`to` هر دو شامل‌اند، سقف **۹۰ روز**. `step_minutes` گام تولید کاندید است
(پیش‌فرض ۱۵، حداقل ۵).

**۲۰۰:**

```json
{
  "success": true,
  "data": {
    "plan": { "total_minutes": 60, "segments": [ … ] },
    "slots": [
      {
        "start": 1785562200,
        "end": 1785565800,
        "assignment": {
          "room":     [{ "uuid": "…", "name": "اتاق ۲" }],
          "operator": [{ "uuid": "…", "name": "اپراتور ۱" }],
          "device":   [{ "uuid": "…", "name": "لیزر ۳" }]
        }
      }
    ],
    "reason": null
  }
}
```

`plan` هم برمی‌گردد تا کلاینت مجبور نباشد جدا `preview` بزند.

**فهرست خالی خطا نیست و ۴۰۴ هم نیست.** `reason: "no_capacity_in_range"` می‌آید تا
کلاینت مجبور نباشد از خالی بودن حدس بزند — ممکن است واقعاً ظرفیتی نباشد.

پاسخ حداکثر **۵۰۰** زمان دارد؛ جستجوی یک‌ماهه نباید هزاران ردیف برگرداند.

### خطاها

| کد | HTTP | کِی |
|---|---|---|
| `ERR_WRONG_BOOKING_MODE` | ۴۲۲ | این محل روی `booking_mode = resource` نیست |
| `ERR_NO_ELIGIBLE_RESOURCE` | ۴۲۲ | هیچ منبعی شرایط یک بخش را ندارد (از برنامه‌ساز) |
| `ERR_VALIDATION_001` | ۴۲۲ | بازهٔ بیش از ۹۰ روز · `to < from` |
| — | ۴۰۴ | سرویس یا شعبهٔ محیط دیگر |

## `GET /api/v1/appointment-availability/month`

`?service_uuid=&branch_uuid=&from=&to=` → فقط `{ "days": [نیمه‌شبِ روزهای دارای ظرفیت] }`.
عمداً سبک است: تقویم ماهانه نباید تخصیص منبع هر زمان را بسازد.

---

## حالت `booking_mode = resource`

مقدار سومِ کنار `slot` و `service`. **افزودنی محض**: پیش‌فرض همچنان `slot` است و هیچ
محیطی خودبه‌خود به این حالت نمی‌رود — ارتقا داوطلبانه و صریح است. محلی که روی این
حالت نرفته باشد، همان `appointment-slots` / `appointment-service-slots` را دارد و
مسیر قدیمی **دست‌نخورده** است.

---

## قواعدی که موتور رعایت می‌کند

| مورد | رفتار |
|---|---|
| `setup/cleanup` منبع | بازهٔ اشغال **گسترده‌تر** از بازهٔ بخش است و در تداخل لحاظ می‌شود |
| ظرفیت منبع | شمارش است نه حضور: اتاق سه‌تخته سه نوبت هم‌زمان می‌گیرد |
| زمان گذشته | حذف می‌شود |
| یک منبع، دو نقش هم‌زمان | مجاز نیست |

### کارایی

هدف مستند: جستجوی یک‌ماهه **زیر نیم ثانیه**.

همهٔ ورودی‌ها یک بار خوانده می‌شوند (تقویم منابع، اشغال‌ها) و بقیه در حافظه است؛ هیچ
کوئری داخل حلقهٔ کاندید یا حلقهٔ روز نیست. کاندیدها هم فقط از پنجره‌های آزادِ
**محدودکننده‌ترین نقش** ساخته می‌شوند — هرس زودهنگام، جستجوی یک‌ماهه را از ده‌ها هزار
کاندید به چند صد می‌رساند.

`tests/Appointment/AvailabilityPerformanceTest.php` این را با ۲۰ منبع و ۵۰۰ نوبت
ثبت‌شده در ۳۰ روز می‌سنجد و بخشی از تسک است، نه اختیاری.

---

## `resource_occupancy`

یک ردیف به‌ازای هر **(بخشِ نوبت × منبع)** — نه یکی به‌ازای کل نوبت. همین ریزدانگی است
که ظرفیت آزاد می‌کند.

`status` ∈ `booked` | `hold`. نوشتن در این جدول کارِ تسک بعدی (رزرو و ثبت) است؛ این
تسک فقط می‌خواندش.

## تست‌ها

```bash
ddev exec php bin/phpunit tests/Appointment/AvailabilityEngineTest.php        # ۹ تست
ddev exec php bin/phpunit tests/Appointment/AvailabilityPerformanceTest.php
```

---

## استراتژی ترتیب منابع

وقتی چند منبع برای یک نقش واجد شرایط‌اند، **ترتیب امتحان‌کردنشان** از تنظیمات محیط
می‌آید (`meta.resource_strategy`). استراتژی فقط مرتب می‌کند؛ تصمیم نهایی همچنان با
موتور است، چون فقط موتور می‌داند کدام منبع در این زمان جا دارد و کدام برای نقش دیگری
برداشته شده.

| کلید | رفتار | کِی مناسب است |
|---|---|---|
| `first_available` | ترتیب نام (پیش‌فرض) | خروجی کاملاً قابل پیش‌بینی |
| `least_gap` | کمترین وقت مردهٔ باقی‌مانده | تقویم کمتر تکه‌تکه شود |
| `least_loaded` | منبعِ آزادتر زودتر | بار بین چند اپراتور پخش شود |
| `same_as_previous` | منبع ترجیحی جلو، بقیه پشت آن | دورهٔ درمان با همان اپراتور |

`least_gap` و `least_loaded` عکس هم عمل می‌کنند و **هر دو درست‌اند**؛ انتخاب بینشان
تصمیم کسب‌وکاری است نه فنی.

### ترجیح منبع دوره

`POST /api/v1/appointment-availability` یک فیلد اختیاری `course_uuid` می‌گیرد. با آن،
`preferred_resource` همان دوره به بالای فهرست می‌رود.

**ترجیح است نه فیلتر:** اگر آن منبع آزاد نباشد، رزرو رد نمی‌شود و به ترتیب پایه
برمی‌گردد — اجبار یعنی بیمار دو هفته منتظر بماند، و آن بدتر از عوض شدن اپراتور است.

### فهرست استراتژی‌ها

`GET /api/v1/appointment-settings/resource-strategies`

```json
{
  "success": true,
  "data": [
    { "code": "first_available", "label": "به ترتیب نام — ساده و قابل پیش‌بینی" },
    { "code": "least_gap", "label": "کمترین وقت مرده — تقویم کمتر تکه‌تکه می‌شود" }
  ]
}
```

انتخابگر پنل از همین ساخته می‌شود؛ افزودن استراتژی تازه یعنی افزودن **یک کلاس** با تگ
`app.resource_picker` — نه تغییر موتور، نه تغییر فرانت.

### رفتار با کلید ناشناخته

هنگام **جستجو** به پیش‌فرض برمی‌گردد (تنظیماتِ قدیمی نباید نوبت‌دهی را بخواباند)، ولی
هنگام **ذخیرهٔ تنظیمات** `422` می‌گیرد — وگرنه کاربر فکر می‌کند استراتژی‌اش اعمال
می‌شود در حالی که نمی‌شود.
