# 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` می‌گیرد — وگرنه کاربر فکر می‌کند استراتژی‌اش اعمال می‌شود در حالی که نمی‌شود.