# تسک ۰۰ — تکمیل نوبت‌دهی سرویسی در 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) کامل‌شده