# تسک ۰۰ — تکمیل نوبت‌دهی سرویسی در clinicpro

**فاز:** ۰ (تثبیت وضعیت فعلی) · **وابستگی:** — · **زمان:** ۱۴-۱۸ ساعت
**پیش‌نیاز همهٔ تسک‌های ۰۱ به بعد**

---

## ⛔ خط سرخ

منطق اسلاتی (`booking_mode = 'slot'`) در این تسک **به هیچ عنوان** دست‌کاری نمی‌شود.
فهرست کامل قفل‌شده‌ها: [_shared/red-lines.md](../_shared/red-lines.md).

این تسک fixture و تست `--group=slot-mode-frozen` را **می‌سازد** — همان تستی که همهٔ
تسک‌های بعدی باید سبز نگهش دارند.

---

## هدف

حالت `booking_mode = 'service'` در مسیر **رزرو** کار می‌کند، ولی در بقیهٔ چرخهٔ عمر نوبت
غایب است. این تسک آن را کامل می‌کند تا موتور چندمنبعی (تسک ۰۶) روی پایهٔ سالم ساخته شود.

## وضعیت فعلی — چه کار می‌کند و چه نمی‌کند

### ✅ کار می‌کند

| مسیر | فایل |
|---|---|
| انتخاب سرویس و اسلات در رزرو عمومی | `GET /api/v1/appointment-booking-services/{doctorUuid}` · `GET /api/v1/appointment-service-slots` |
| محاسبهٔ زمان‌های شروع بر اساس مدت سرویس | `SlotCalculatorService::getServiceStartTimes()` |
| ثبت نوبت با چند سرویس | `POST /api/v1/appointment` + `appointment_service_items` |
| توگل روش نوبت‌دهی در تنظیمات | `assets/admin/components/schedule/ScheduleSection.tsx` |
| ساخت نوبت از پنل | `assets/admin/pages/AppointmentCreatePage.tsx` + `components/appointments/ServiceSlotPicker.tsx` + `hooks/useDoctorBookingServices.ts` |
| ساخت سریع از drawer | `assets/admin/components/NewAppointmentDrawer.tsx` |
| رزرو از سایت | `nobat724_front/components/appointment/*` |

### ❌ کار نمی‌کند — شکاف‌های این تسک

**۱. ویرایش و جابه‌جایی نوبت، حالت سرویسی را نمی‌شناسد.**

`PATCH /api/v1/appointment/{uuid}` ([AppointmentController.php:1077](../../../src/Appointment/Controller/AppointmentController.php#L1077)):

```php
$hasStart = array_key_exists('slot_start', $data);
$hasEnd   = array_key_exists('slot_end', $data);
// … فقط این دو بررسی می‌شوند:
if ($newEnd <= $newStart) { /* 422 */ }
if ($this->appointmentRepo->isSlotTaken($doctor, $newStart, $newEnd, $id)) { /* 409 */ }
```

سه مشکل:
- مدت دلخواه پذیرفته می‌شود؛ هیچ بررسی‌ای که `slot_end - slot_start` با مجموع مدت
  سرویس‌های نوبت بخواند وجود ندارد
- `buffer_minutes` نادیده گرفته می‌شود — نوبت جدید می‌تواند چسبیده به نوبت بعدی بنشیند
- فقط `service_item_uuid` تکی به‌روز می‌شود؛ `service_items` (ManyToMany) دست‌نخورده
  می‌ماند → نوبت با سرویس‌های قبلی و مدت جدید ناسازگار می‌شود

**۲. `AppointmentEditPage.tsx` ورودی دستی ساعت دارد.**

سه فیلد `date`/`start`/`end` آزاد + یک `SearchableSelect` تکی برای سرویس
([AppointmentEditPage.tsx:74-76](../../../assets/admin/pages/AppointmentEditPage.tsx#L74)).
هیچ `ServiceSlotPicker` ای نیست، هیچ چند-سرویسی نیست.

نتیجه: منشی نوبت سرویسیِ ۴۵ دقیقه‌ای را ویرایش می‌کند، ۲۰ دقیقه می‌گذارد، سیستم قبول
می‌کند، و بیمار بعدی روی نوبت اول می‌نشیند.

**۳. نوبت رزرو (`is_reserve`) در حالت سرویسی معنا ندارد.**

`NewAppointmentDrawer.tsx:72` صریح: `$serviceMode = bookingMode === 'service' && !isReserve`.
پس نوبت رزرو همیشه اسلاتی رفتار می‌کند و `ReserveAppointmentsPage.tsx` فقط
`service_item?.name` تکی نشان می‌دهد. تبدیل رزرو به نوبت واقعی هم مسیر سرویسی ندارد.

**۴. `patient_facing` بودن مدت جایی نمایش داده نمی‌شود.**

پاسخ `appointment-service-slots` مدت کل را می‌دهد ولی نوبت ثبت‌شده هیچ‌جا نگه نمی‌دارد
که این مدت از کدام سرویس‌ها و چه بافری آمده. لیست نوبت‌ها فقط `slot_start/slot_end` دارد.

## دامنه

**هست:**
- `ServiceBookingCalculator` — یک سرویس واحد که «مدت مجاز یک ترکیب سرویس» را حساب می‌کند
  (استخراج منطق تکرارشدهٔ `serviceSlots()` از کنترلر)
- اعتبارسنجی حالت سرویسی در `PATCH /appointment/{uuid}`
- endpoint جابه‌جایی سرویس‌آگاه: `POST /api/v1/appointment/{uuid}/service-reschedule`
- `ServiceSlotPicker` در `AppointmentEditPage`
- حالت سرویسی برای نوبت رزرو + تبدیل رزرو به نوبت
- ستون‌های `service_total_minutes` و `service_buffer_minutes` روی `appointments`
- fixture و تست `--group=slot-mode-frozen`

**نیست:** بخش‌های نوبت، چند منبع، قوانین (تسک ۰۵ به بعد). `nobat724_front` (تسک ۰۰ب).

## Endpoint ها

| متد | مسیر | توضیح |
|---|---|---|
| POST | `/api/v1/appointment/{uuid}/service-reschedule` | جابه‌جایی سرویس‌آگاه: سرویس‌ها + زمان شروع؛ مدت را خودش حساب می‌کند |
| PATCH | `/api/v1/appointment/{uuid}` | **توسعه** — در حالت سرویسی مدت را اعتبارسنجی می‌کند و `service_item_uuids[]` می‌پذیرد |
| POST | `/api/v1/appointment/{uuid}/convert-reserve` | تبدیل نوبت رزرو به نوبت زمان‌دار (هر دو حالت) |
| GET | `/api/v1/appointment-service-slots` | **توسعه** — پارامتر `exclude_appointment_uuid` برای جابه‌جایی |

هیچ endpoint اسلاتی‌ای تغییر نمی‌کند. `GET /appointment-slots` دست‌نخورده.

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

- ✅ موفق: نوبت سرویسیِ «لیزر صورت (۲۰) + بیکینی (۱۵)» با مدت ۳۵ دقیقه.
  `POST /appointment/{uuid}/service-reschedule` با زمان جدید و همان سرویس‌ها →
  `200` و `slot_end - slot_start = 35 * 60` دقیقاً.
- ✅ موفق: همان endpoint با حذف بیکینی → مدت خودکار ۲۰ دقیقه می‌شود، بدون اینکه کلاینت
  عددی بفرستد.
- ✅ موفق: `GET /appointment-service-slots?…&exclude_appointment_uuid={uuid}` بازهٔ خودِ
  نوبت را اشغال حساب نمی‌کند، پس زمان فعلی‌اش در فهرست می‌آید.
- ✅ موفق: `AppointmentEditPage` برای نوبت سرویسی، `ServiceSlotPicker` نشان می‌دهد و
  ورودی دستی ساعت را **پنهان** می‌کند؛ برای نوبت اسلاتی، دقیقاً رفتار امروز.
- ✅ موفق: نوبت رزرو در حالت سرویسی سرویس‌هایش را ذخیره می‌کند و
  `POST /convert-reserve` با زمان انتخابی، نوبت زمان‌دار با مدت درست می‌سازد.
- ✅ موفق (**خط سرخ**): `ddev exec php bin/phpunit --group=slot-mode-frozen` سبز است و
  fixture قرارداد اسلاتی بیت‌به‌بیت تغییر نکرده.
- ❌ خطا: `PATCH` با `slot_end - slot_start` ناسازگار با مدت سرویس‌ها →
  `422` `ERR_SERVICE_DURATION_MISMATCH` با پیام فارسی شامل مدت درست.
- ❌ خطا: `service-reschedule` روی نوبت **اسلاتی** → `422` `ERR_WRONG_BOOKING_MODE`.
- ❌ خطا: `service-reschedule` با زمان شروعی که در `getServiceStartTimes` نیست →
  `422` با پیام «این زمان برای مدت انتخابی در دسترس نیست».
- ❌ خطا: سرویس محیط دیگر در `service_item_uuids[]` → `404` (بدون لو دادن وجودش).
- ⚠️ مرزی: نوبتی که سرویس‌هایش غیرفعال (`bookable=false`) شده‌اند → جابه‌جایی مجاز است
  با `warnings[]`؛ افزودن سرویس غیرفعال ممنوع.
- ⚠️ مرزی: `buffer_minutes` تغییر کرد بعد از ثبت نوبت → نوبت موجود سالم می‌ماند؛
  فقط جابه‌جایی جدید بافر جدید را می‌گیرد.
- ⚠️ مرزی: جابه‌جایی به روزی که برنامهٔ هفتگی آن محیط عوض شده → همان اعتبارسنجی
  `getServiceStartTimes`، پس خودکار پوشش داده می‌شود.
- ⚠️ مرزی: نوبت سرویسی بدون هیچ سرویس (داده قدیمی) → مدت موجود حفظ می‌شود و
  `warnings[]` می‌گوید سرویس ثبت نشده. **رد نمی‌شود.**
- ⚠️ مرزی: `PATCH` بدون `slot_start` روی نوبت سرویسی (فقط تغییر یادداشت) → بدون
  اعتبارسنجی مدت، مثل امروز.

## خروجی

- `src/Appointment/Service/ServiceBookingCalculator.php` + توسعهٔ کنترلر
- `assets/admin/pages/AppointmentEditPage.tsx` توسعه‌یافته
- `assets/admin/pages/ReserveAppointmentsPage.tsx` توسعه‌یافته
- migration دو ستون تهی‌پذیر
- `tests/Appointment/SlotModeFrozenTest.php` + fixture
- `docs/api/appointment.md` به‌روزرسانی
- [checklist.md](checklist.md) کامل‌شده
