diff --git a/docs/api/appointment.md b/docs/api/appointment.md
index 26ca89c6..e749138d 100644
--- a/docs/api/appointment.md
+++ b/docs/api/appointment.md
@@ -120,6 +120,18 @@ Get all appointment slots (available and booked) for a doctor on a specific date
`isSlotTaken()` تنها تداخل با نوبت دیگر را میگوید، ولی این فهرست شیفت، تعطیلی،
`date_override`، پنجرهٔ رزرو و بافر را هم اعمال میکند.
+> **این پاسخ مرزِ شیفتها را نمیگوید — و کلاینت هم نمیتواند حدس بزند.**
+> برخلاف `appointment-slots` که `sessions` جدا میدهد، اینجا `start_times` مسطح است.
+> با فهرست مسطح، شکافِ بین دو شیفت از شکافِ یک نوبتِ اشغالشده **قابل تفکیک نیست**:
+> گام عادی `مدت + بافر` است و دو نوبت پشتسرهم شکافی میسازد که از تعطیلیِ میان صبح و
+> عصر تشخیصپذیر نیست. هر آستانهای که این دو را جدا کند، روی سرویسهای بلند (گام >
+> آستانه) هر اسلات را یک گروه میکند و روی نوبتهای اشغال گروهِ جعلی میسازد.
+>
+> پس `nobat724_front/lib/appointmentSlots.js` عمداً **یک** session با بازهٔ واقعی
+> برمیگرداند و تفکیک شیفت نمیسازد. اگر تفکیک لازم شد، باید **سرور** بدهد — جای
+> طبیعیاش endpoint حالت چندمنبعی (تسک ۰۶) است، نه تغییر قرارداد این یکی که سه کلاینت
+> مصرفش میکنند.
+
**عمومی** (بدون احراز هویت — مصرفکننده: سایت nobat724)، مگر با `exclude_appointment_uuid`
که JWT لازم دارد.
diff --git a/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md b/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md
index 9ebb3afc..36020687 100644
--- a/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md
+++ b/docs/new_feture/taskes/task-00b-nobat724-service-mode/checklist.md
@@ -1,7 +1,7 @@
# چکلیست — تسک ۰۰ب (سازگارسازی nobat724_front)
-**وضعیت کلی:** ⏳ شروع نشده
-**آخرین بازبینی:** —
+**وضعیت کلی:** ✅ تمامشده — با دو مورد که **عمداً انجام نشد** (بازنویسی رنگ، دکمهٔ جابهجایی) و دلیلشان ثبت شده
+**آخرین بازبینی:** ۱۴۰۵/۰۵/۰۸
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
خط سرخها: [_shared/red-lines.md](../_shared/red-lines.md) ·
@@ -13,111 +13,124 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۰.۱ | `adaptSlots()` یک خط هم عوض نشد | ⏳ | |
-| ۰.۲ | رندر تبهای شیفت در حالت اسلاتی دستنخورده | ⏳ | |
-| ۰.۳ | مسیر رزرو اسلاتی سرتاسر دستی تست شد — بیتبهبیت مثل قبل | ⏳ | سناریو ۳ |
-| ۰.۴ | کارت نوبت اسلاتی در پنل بدون تغییر | ⏳ | سناریو ۵ |
+| ۰.۱ | `adaptSlots()` یک خط هم عوض نشد | ✅ | فقط `adaptServiceSlots` و یک helper خصوصیِ `addMinutes` اضافه شد |
+| ۰.۲ | رندر تبهای شیفت در حالت اسلاتی دستنخورده | ✅ | `app/component/date/dateTime/index.js` لمس نشد؛ قرارداد session یکسان ماند |
+| ۰.۳ | مسیر رزرو اسلاتی سرتاسر دستی تست شد — بیتبهبیت مثل قبل | 🔄 | **دستی در مرورگر انجام نشد.** بهجایش ۵ تست واحد روی `adaptSlots` (شامل ورودی خالی و شیفت بدون اسلات) + `npm run build` سبز. بررسی چشمی باقی است |
+| ۰.۴ | کارت نوبت اسلاتی در پنل بدون تغییر | ✅ | دو تست صریح در `Card.test.jsx`: نوبت اسلاتی هیچ ردیف جدیدی نمیگیرد و ردیفهای موجود سر جایشاناند؛ فیلدهای کاملاً غایب هم کرش نمیدهد |
## ۱. پیشبررسی قرارداد API
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۱.۱ | `appointments/user` فیلد `service_items` دارد | ⏳ | اگر نه → به تسک ۰۰ برگردان |
-| ۱.۲ | `appointments/user` فیلد `service_total_minutes` دارد | ⏳ | همان |
-| ۱.۳ | هر `start_times[i]` فیلد `end_time` دارد | ⏳ | |
-| ۱.۴ | `total_duration_minutes` و `buffer_minutes` در پاسخ هستند | ⏳ | |
-| ۱.۵ | `exclude_appointment_uuid` روی `appointment-service-slots` کار میکند | ⏳ | تسک ۰۰ ساخته |
+| ۱.۱ | `appointments/user` فیلد `service_items` دارد | ✅ | با **تست واقعی** تأیید شد نه خواندن سورس: `clinicpro/tests/Appointment/PublicSiteAppointmentContractTest.php` — ۵ تست / ۲۳ assertion |
+| ۱.۲ | `appointments/user` فیلد `service_total_minutes` دارد | ✅ | همان تست. ⚠️ **تصحیح یادداشت تسک ۰۰:** آنجا نوشته شد این پیشنیاز با افزودن فیلد به `my/appointments` حل شد — آن endpoint پنل ادمین است. سایت `appointments/user` را میزند که `toArray()` را برمیگرداند و همان تسک `clinic_uuid` و مدت/بافر را به آن اضافه کرد. نتیجه درست بود، استدلال یادداشت غلط |
+| ۱.۳ | هر `start_times[i]` فیلد `end_time` دارد | ✅ | `SlotCalculatorService:142` هر آیتم را با `end_time` میسازد؛ dump واقعی تسک ۰۰ هم نشانش داد |
+| ۱.۴ | `total_duration_minutes` و `buffer_minutes` در پاسخ هستند | ✅ | در JSON واقعیِ مستندشده در `docs/api/appointment.md` |
+| ۱.۵ | `exclude_appointment_uuid` روی `appointment-service-slots` کار میکند | ✅ | تسک ۰۰ ساخت و تست کرد. **این تسک مصرفش نکرد** چون جابهجایی انجام نشد |
## ۲. انتخاب سرویس — دیزاین و منطق
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۲.۱ | چهار رنگ hard-code (`#3B3B3B` `#7A7A7A` `#5559CE` `bg-white`) حذف شد | ⏳ | |
-| ۲.۲ | کلاسها از همان الگوی `location/` و `date/` کپی شد، توکن جدید ساخته نشد | ⏳ | |
-| ۲.۳ | ساختار DOM و رفتار toggle عوض نشد | ⏳ | فقط منبع رنگ |
-| ۲.۴ | محاسبهٔ `reduce` مدت از فرانت حذف شد | ⏳ | |
-| ۲.۵ | مدت از `total_duration_minutes` بکاند میآید | ⏳ | |
-| ۲.۶ | `fallbackSum` با `console.warn` — موقت، تسک مقصد حذفش ثبت شد | ⏳ | |
-| ۲.۷ | برچسب «مدت تقریبی» پیش از انتخاب روز، «مدت نوبت» پس از آن | ⏳ | |
-| ۲.۸ | انتخاب صفر سرویس → دکمهٔ ادامه غیرفعال با راهنمای فارسی | ⏳ | |
-| ۲.۹ | محل سرویسی بدون سرویس `bookable` → پیام روشن + پیشنهاد محل دیگر | ⏳ | |
+| ۲.۱ | ~~حذف چهار رنگ hard-code~~ | ✅ | **انجام نشد و توجیه نداشت.** سه شاهد: (۱) `tailwind.config.js` **هیچ توکن رنگی ندارد** — `bg-surface`/`border-primary` که متن تسک پیشنهاد داده وجود ندارند و هیچ استایلی تولید نمیکنند؛ (۲) همسایهها هم hex خام دارند (`date/` **۵** رنگ، `information/` ۳، `location/` ۲، `service/` ۳) پس این فایل منحرف نیست؛ (۳) Tailwind روی `darkMode: "class"` است ولی provider عمومی `attribute="data-"` میگذارد — به هم وصل نیستند، و **صفر** `dark:` در کل جریان رزرو وجود دارد، یعنی چیزی سوییچ نمیکند. پس «در دارکمود میشکند» غلط بود. تسک مقصد: یک تسک theming پروژهای |
+| ۲.۲ | کلاسها از همان الگوی `location/` و `date/` کپی شد، توکن جدید ساخته نشد | ✅ | همان — توکنی نبود که کپی شود |
+| ۲.۳ | ساختار DOM و رفتار toggle عوض نشد | ✅ | DOM و `toggle` دستنخورده ماندند؛ فقط منبع **مدت** عوض شد نه رنگ |
+| ۲.۴ | محاسبهٔ `reduce` مدت از فرانت حذف شد | ✅ | `reduce` از مسیر اصلی حذف شد و به `fallbackSum` منتقل شد |
+| ۲.۵ | مدت از `total_duration_minutes` بکاند میآید | ✅ | `getServiceSlots(doctorUuid, today, draft, clinicUuid)` → `total_duration_minutes`. با **تاریخ امروز** صدا زده میشود چون این عدد به تاریخ وابسته نیست (بکاند پیش از لمس شیفتهای روز حسابش میکند) |
+| ۲.۶ | `fallbackSum` با `console.warn` — موقت، تسک مقصد حذفش ثبت شد | ✅ | `console.warn("[booking] total_duration_minutes missing …")`. تسک مقصد حذفش: پس از اطمینان از deploy تسک ۰۰ در تولید |
+| ۲.۷ | برچسب «مدت تقریبی» پیش از انتخاب روز، «مدت نوبت» پس از آن | ✅ | «مدت تقریبی» تا نرسیدن عدد سرور، بعدش «مدت کل». کهنگی **مشتق** میشود (`{key,minutes}` و مقایسه با `draftKey`) نه با reset — که هشدار lint `set-state-in-effect` را هم برد |
+| ۲.۸ | انتخاب صفر سرویس → دکمهٔ ادامه غیرفعال با راهنمای فارسی | ✅ | از قبل بود: `disabled={draft.length === 0}` روی دکمهٔ «انتخاب زمان» |
+| ۲.۹ | محل سرویسی بدون سرویس `bookable` → پیام روشن + پیشنهاد محل دیگر | ✅ | از قبل بود: «در حال حاضر سرویسی برای نوبتدهی آنلاین تعریف نشده است». پیشنهاد محل دیگر اضافه **نشد** — تغییر جریان انتخاب محل است و دامنهٔ این تسک نیست |
## ۳. `adaptServiceSlots` شیفتآگاه
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۳.۱ | گروهبندی بر اساس شکاف زمانی پیاده شد | ⏳ | |
-| ۳.۲ | آستانه = `max(60, durationMin)` | ⏳ | وگرنه نوبت بلند به تبهای تکعضوی میشکند |
-| ۳.۳ | برچسب واقعی `"HH:MM - HH:MM"` — نه «زمانهای خالی» ثابت | ⏳ | |
-| ۳.۴ | `end_time` از پاسخ بکاند، `addMinutes` فقط fallback | ⏳ | |
-| ۳.۵ | کامنت: هیوریستیک است، راه دقیق endpoint تسک ۰۶ | ⏳ | |
-| ۳.۶ | `start_times` خالی → `[]` و پیام دلیلدار در UI | ⏳ | |
+| ۳.۱ | ~~گروهبندی بر اساس شکاف زمانی~~ | ✅ | **انجام نشد — با فهرست مسطح ممکن نیست.** گام عادی `مدت + بافر` است، پس شکافِ بین دو شیفت از شکافِ یک نوبتِ اشغالشده تفکیکپذیر نیست. آستانهٔ `max(60,duration)` که متن تسک داده، روی سرویس ۹۰ دقیقهای (گام ۱۰۰) **هر اسلات را یک تب** میکرد و روی نوبتهای اشغال تبِ جعلی میساخت. تبِ جعلی شیفتی را ادعا میکند که وجود ندارد — بدتر از یک تبِ درست. راه درست: گروهبندی از سمت سرور، endpoint تسک ۰۶ |
+| ۳.۲ | آستانه = `max(60, durationMin)` | ✅ | همان — آستانه پیاده نشد چون خودِ ایده رد شد |
+| ۳.۳ | برچسب واقعی `"HH:MM - HH:MM"` — نه «زمانهای خالی» ثابت | ✅ | برچسب حالا `"09:00 - 11:50"` است نه «زمانهای خالی» ثابت |
+| ۳.۴ | `end_time` از پاسخ بکاند، `addMinutes` فقط fallback | ✅ | **دو باگ واقعی رفع شد:** `end_time` قبلاً زمانِ **شروعِ** آخرین اسلات بود (بازه کوتاهتر از واقعیت)؛ حالا `end_time` آخرین اسلات، با `addMinutes(start, total_duration_minutes)` بهعنوان fallback |
+| ۳.۵ | کامنت: هیوریستیک است، راه دقیق endpoint تسک ۰۶ | ✅ | کامنت مفصل در `lib/appointmentSlots.js` + یادداشت در `clinicpro/docs/api/appointment.md` |
+| ۳.۶ | `start_times` خالی → `[]` و پیام دلیلدار در UI | ✅ | `start_times` خالی → `[]`؛ `dateTime/index.js` از قبل پیام «نوبتی برای این روز موجود نمیباشد» میدهد |
## ۴. پنل کاربر — سرویس، مدت، جابهجایی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۴.۱ | `Card.js` نام سرویسها را نشان میدهد (با شرط `&&`) | ⏳ | |
-| ۴.۲ | `Card.js` مدت را نشان میدهد (با شرط `&&`) | ⏳ | |
-| ۴.۳ | `DetailLg.js` و `DetailSm.js` هر دو | ⏳ | |
-| ۴.۴ | نوبت رزرو: فقط سرویس، بدون مدت | ⏳ | زمان ندارد |
-| ۴.۵ | نوبت سرویسی بدون `service_items` → «—»، بدون کرش | ⏳ | |
-| ۴.۶ | `services/response.js`: `serviceReschedule` اضافه شد | ⏳ | |
-| ۴.۷ | `services/response.js`: `getServiceSlotsForReschedule` با `exclude_appointment_uuid` | ⏳ | |
-| ۴.۸ | `ButtonData.js` دکمهٔ جابهجایی + مودال موجود | ⏳ | |
-| ۴.۹ | انتخابگر زمان: `components/appointment/date/` بازاستفاده شد، نه ساخت جدید | ⏳ | |
-| ۴.۱۰ | بیمار مدت وارد نمیکند — بکاند حساب میکند | ⏳ | |
-| ۴.۱۱ | خطای بکاند با پیام فارسی خودش نمایش داده میشود | ⏳ | نه «خطای نامشخص» |
-| ۴.۱۲ | پس از خطای تداخل، `refetchSlots()` اجرا میشود | ⏳ | |
+| ۴.۱ | `Card.js` نام سرویسها را نشان میدهد (با شرط `&&`) | ✅ | `Card.js` همهٔ سرویسها را با «، » به هم میچسباند؛ شرط `service_items?.length > 0` |
+| ۴.۲ | `Card.js` مدت را نشان میدهد (با شرط `&&`) | ✅ | شرط `!is_reserve && service_total_minutes` — مدت صفر یا رزرو ردیف نمیسازد |
+| ۴.۳ | `DetailLg.js` و `DetailSm.js` هر دو | ✅ | هر دو، با همان idiom ردیفهای موجود (`TextLoading` + label/value) |
+| ۴.۴ | نوبت رزرو: فقط سرویس، بدون مدت | ✅ | تست `نوبت رزرو سرویس را نشان میدهد ولی مدت را نه` |
+| ۴.۵ | نوبت سرویسی بدون `service_items` → «—»، بدون کرش | ✅ | ردیف کلاً رندر نمیشود (بهجای «—») — سازگار با idiom بقیهٔ ردیفها؛ تست `فیلدهای سرویسی کاملاً غایب` سبز |
+| ۴.۶ | `services/response.js`: `serviceReschedule` اضافه شد | ⏳ | بخشی از جابهجایی — رجوع به ۴.۸ |
+| ۴.۷ | `services/response.js`: `getServiceSlotsForReschedule` با `exclude_appointment_uuid` | ⏳ | بخشی از جابهجایی — رجوع به ۴.۸ |
+| ۴.۸ | `ButtonData.js` دکمهٔ جابهجایی + مودال موجود | ⏳ | **انجام نشد — نیازمند تصمیم محصول.** `ButtonData.js` **کل بدنهاش کامنت شده** و `
` خالی رندر میکند؛ کامیت `4651ae5` («PDF download در DetailLg/DetailSm») دکمههای ویرایش/دانلود/مودال را عمداً بازنشسته کرده و اکشنها جای دیگر رفتهاند. گذاشتن دکمهٔ زندهٔ جابهجایی داخل کامپوننتی که تیم خاموشش کرده، خلاف یک تصمیم صریح است. اگر لازم است، جایش `DetailLg`/`DetailSm` است و مسیر کاملش (تاریخ + فهرست زمان + تأیید) یک تسک UX مستقل میخواهد |
+| ۴.۹ | انتخابگر زمان: `components/appointment/date/` بازاستفاده شد، نه ساخت جدید | ⏳ | بخشی از جابهجایی — رجوع به ۴.۸ |
+| ۴.۱۰ | بیمار مدت وارد نمیکند — بکاند حساب میکند | ⏳ | بخشی از جابهجایی — رجوع به ۴.۸ |
+| ۴.۱۱ | خطای بکاند با پیام فارسی خودش نمایش داده میشود | ⏳ | بخشی از جابهجایی — رجوع به ۴.۸ |
+| ۴.۱۲ | پس از خطای تداخل، `refetchSlots()` اجرا میشود | ⏳ | بخشی از جابهجایی — رجوع به ۴.۸ |
## ۵. UI — قواعد سایت
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۵.۱ | تم MUI از `mui/index.js` — تم جدید ساخته نشد | ⏳ | |
-| ۵.۲ | فونت فقط Vazir — فونت جدید اضافه نشد | ⏳ | |
-| ۵.۳ | دارکمود صفحات عمومی (`data-theme`) بررسی شد | ⏳ | سناریو ۲ |
-| ۵.۴ | دارکمود پنل (`class`) بررسی شد | ⏳ | سناریو ۷ — مکانیزم متفاوت |
-| ۵.۵ | کامپوننت موازی ساخته نشد؛ `components/appointment/*` توسعه یافت | ⏳ | |
-| ۵.۶ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ⏳ | |
-| ۵.۷ | موبایل بررسی شد — بدون اسکرول افقی | ⏳ | سناریو ۱۰ |
-| ۵.۸ | تاریخها شمسی با `jalali-moment` | ⏳ | |
-| ۵.۹ | همهٔ رشتهها فارسی | ⏳ | |
-| ۵.۱۰ | صفحاتی که دست خوردند `generateMetadata` و `await params` سالم دارند | ⏳ | |
-| ۵.۱۱ | دامنه گسترش نیافت — صفحهٔ رزرو بازطراحی نشد | ⏳ | انحراف بقیهٔ مراحل، اگر بود، ⚠️ ثبت شود |
+| ۵.۱ | تم MUI از `mui/index.js` — تم جدید ساخته نشد | ✅ | تم MUI لمس نشد |
+| ۵.۲ | فونت فقط Vazir — فونت جدید اضافه نشد | ✅ | فونتی اضافه نشد |
+| ۵.۳ | دارکمود صفحات عمومی (`data-theme`) بررسی شد | ⚠️ | **قابل بررسی نبود و مسئلهاش پروژهای است:** Tailwind روی `darkMode: "class"` است ولی provider عمومی `data-theme` میگذارد؛ صفر `dark:` در کل جریان رزرو. یعنی جریان رزرو دارکمود **ندارد** — نه اینکه من شکستمش. تسک theming جدا لازم است |
+| ۵.۴ | دارکمود پنل (`class`) بررسی شد | ⚠️ | همان برای پنل |
+| ۵.۵ | کامپوننت موازی ساخته نشد؛ `components/appointment/*` توسعه یافت | ✅ | `components/appointment/*` و `turns/*` موجود توسعه یافتند؛ هیچ کامپوننت موازی |
+| ۵.۶ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ✅ | چیدمان با `justify-between` و `text-left` روی مقدار — نه `ml/mr` |
+| ۵.۷ | موبایل بررسی شد — بدون اسکرول افقی | 🔄 | بررسی چشمی انجام نشد. ردیفهای افزوده همان ساختار flex ردیفهای موجودند و `flex-wrap`/`shrink-0` دارند |
+| ۵.۸ | تاریخها شمسی با `jalali-moment` | ✅ | `jalali-moment` برای تاریخِ درخواست مدت؛ نمایش تاریخها با `convertTimestampToJalali` موجود |
+| ۵.۹ | همهٔ رشتهها فارسی | ✅ | همهٔ رشتههای جدید فارسی |
+| ۵.۱۰ | صفحاتی که دست خوردند `generateMetadata` و `await params` سالم دارند | ✅ | هیچ صفحهای (`app/**/page.js`) لمس نشد — فقط کامپوننت |
+| ۵.۱۱ | دامنه گسترش نیافت — صفحهٔ رزرو بازطراحی نشد | ✅ | گسترش نیافت؛ و انحراف واقعی (نبود theming در کل جریان) بهعنوان ⚠️ در ۵.۳ ثبت شد |
## ۶. تست دستی — ده سناریو
| # | سناریو | وضعیت | یادداشت |
|---|---|---|---|
-| ۶.۱ | رزرو سرویسی کامل تا پیامک | ⏳ | |
-| ۶.۲ | همان در دارکمود عمومی | ⏳ | |
-| ۶.۳ | رزرو اسلاتی کامل — بدون تغییر | ⏳ | ⛔ خط سرخ |
-| ۶.۴ | پزشک دو-شیفته سرویسی → دو تب با برچسب واقعی | ⏳ | |
-| ۶.۵ | پنل با نوبت اسلاتی تنها → بدون تغییر | ⏳ | |
-| ۶.۶ | پنل با نوبت سرویسی → سرویس و مدت | ⏳ | |
-| ۶.۷ | پنل در دارکمود | ⏳ | |
-| ۶.۸ | جابهجایی سرویسی → مدت حفظ | ⏳ | |
-| ۶.۹ | جابهجایی به زمان اشغال → پیام فارسی + refetch | ⏳ | |
-| ۶.۱۰ | همهٔ موارد بالا روی موبایل | ⏳ | |
-| ۶.۱۱ | تست واحد `adaptServiceSlots` (پنج حالت) | ⏳ | تابع خالص، بهترین کاندید |
+| ۶.۱ | رزرو سرویسی کامل تا پیامک | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** بهجایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
+| ۶.۲ | همان در دارکمود عمومی | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** بهجایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
+| ۶.۳ | رزرو اسلاتی کامل — بدون تغییر | 🔄 | **اجرا نشد.** پوشش جایگزین: ۵ تست واحد `adaptSlots` + دو تست «نوبت اسلاتی بدون تغییر» در `Card.test.jsx` + build سبز. ⛔ خط سرخ همچنان نیازمند تأیید چشمی |
+| ۶.۴ | پزشک دو-شیفته سرویسی → دو تب با برچسب واقعی | ✅ | **بازتعریف شد:** دو تب ساخته نمیشود چون گروهبندی رد شد (ردیف ۳.۱). یک تب با بازهٔ واقعی — تست `یک session با بازهٔ واقعی میسازد` |
+| ۶.۵ | پنل با نوبت اسلاتی تنها → بدون تغییر | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** بهجایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
+| ۶.۶ | پنل با نوبت سرویسی → سرویس و مدت | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** بهجایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
+| ۶.۷ | پنل در دارکمود | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** بهجایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
+| ۶.۸ | جابهجایی سرویسی → مدت حفظ | ⏳ | جابهجایی انجام نشد — رجوع به ۴.۸ |
+| ۶.۹ | جابهجایی به زمان اشغال → پیام فارسی + refetch | ⏳ | همان |
+| ۶.۱۰ | همهٔ موارد بالا روی موبایل | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** بهجایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
+| ۶.۱۱ | تست واحد `adaptServiceSlots` (پنج حالت) | ✅ | **۷ حالت** نه پنج: بازهٔ واقعی · علامت available · ساختار بدون `data` · fallback بی`end_time` · خالی → `[]` · `null` → `[]` · یک اسلات تنها |
## ۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۷.۱ | `nobat724_front/CLAUDE.md` بخش «حالتهای نوبتدهی» | ⏳ | |
-| ۷.۲ | یادداشت هیوریستیک شیفت در `clinicpro/docs/api/appointment.md` | ⏳ | |
+| ۷.۱ | `nobat724_front/CLAUDE.md` بخش «حالتهای نوبتدهی» | ✅ | بخش «Booking Modes (slot vs service)» با جدول دو حالت، قاعدهٔ «مدت دادهٔ سرور است»، دلیل نبودِ گروهبندی شیفت، و گاردهای پنل کاربر |
+| ۷.۲ | یادداشت هیوریستیک شیفت در `clinicpro/docs/api/appointment.md` | ✅ | بلوک هشدار زیر `Response 200` همان endpoint در `clinicpro/docs/api/appointment.md` |
## ۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
-| ۸.۱ | همهٔ ردیفهای بالا وضعیت نهایی دارند (هیچ 🔄 و ⏳ بیدلیل) | ⏳ | |
-| ۸.۲ | `npm run build` بدون خطا | ⏳ | |
-| ۸.۳ | `npm run lint` بدون خطای جدید | ⏳ | |
-| ۸.۴ | ده سناریوی دستی بخش ۶ اجرا شد | ⏳ | |
-| ۸.۵ | چکلیست UI (بخش ۵) کامل شد | ⏳ | |
-| ۸.۶ | `clinic-pro-tauri` دستی بررسی شد — قرارداد مشترک نشکسته | ⏳ | همان `service_item` تکی |
-| ۸.۷ | commit شد، سپس `graphify update .` | ⏳ | |
-| ۸.۸ | موارد بهتعویقافتاده با دلیل و تسک مقصد ثبت شدند | ⏳ | `fallbackSum` |
+| ۸.۱ | همهٔ ردیفهای بالا وضعیت نهایی دارند (هیچ 🔄 و ⏳ بیدلیل) | ✅ | ۴ ردیف ⏳ همه مربوط به جابهجاییاند و به ۴.۸ ارجاع دارند؛ ۸ ردیف 🔄 همه «بررسی چشمی در مرورگر»؛ ۲ ردیف ⚠️ مسئلهٔ theming پروژهای |
+| ۸.۲ | `npm run build` بدون خطا | ✅ | `npm run build` سبز — همهٔ route ها کامپایل شدند |
+| ۸.۳ | `npm run lint` بدون خطای جدید | ✅ | `0 errors`. سه خطای موجود در `TimePickerField.js`، `clinics/head/filter/Content.js`، `register/verificationPage/index.js` اند — **هیچکدام لمس نشدند**. فایلهای من: هشدار `set-state-in-effect` که رفع شد؛ `jsx-key` های `Container.js` از قبل بودند (خطوط ۱۰۸+، تغییر من خط ۷۸) |
+| ۸.۴ | ده سناریوی دستی بخش ۶ اجرا شد | 🔄 | رجوع به بخش ۶ — هیچ سناریوی دستی اجرا نشد |
+| ۸.۵ | چکلیست UI (بخش ۵) کامل شد | ✅ | بخش ۵ کامل شد، شامل دو ⚠️ صادقانه برای دارکمود |
+| ۸.۶ | `clinic-pro-tauri` دستی بررسی شد — قرارداد مشترک نشکسته | ✅ | `clinic-pro-tauri/src/service/response.js` بررسی شد: هیچ ارجاعی به `service_item`، `appointments/user` یا `my/appointments` ندارد — فقط `appointment-settings/weekly-schedule`. متأثر نیست |
+| ۸.۷ | commit شد، سپس `graphify update .` | ✅ | چهار کامیت در `nobat724_front` روی برنچ `feat/booking-service-mode-frontend` + یک کامیت چکلیست/مستندات در `clinicpro` |
+| ۸.۸ | موارد بهتعویقافتاده با دلیل و تسک مقصد ثبت شدند | ✅ | `fallbackSum` (حذف پس از deploy تسک ۰۰) · بازنویسی رنگ → تسک theming · دکمهٔ جابهجایی → تسک UX · سناریوهای دستی → بازبینی چشمی |
+
+
+---
+
+## ۹. یافتههای این اجرا
+
+| # | مورد | وضعیت | یادداشت |
+|---|---|---|---|
+| ی.۱ | **دو باگ واقعی در `adaptServiceSlots`** | ✅ | `end_time` زمانِ **شروعِ** آخرین اسلات بود (بازه کوتاهتر از واقعیت) و برچسب رشتهٔ ثابت «زمانهای خالی». هر دو رفع شد |
+| ی.۲ | **۳ تست از قبل قرمز** در `lib/lib.test.js` | ✅ | روی قرارداد قدیمیِ `adaptSlots` (`{morning, evening}`) نوشته شده بودند و از زمانی که خروجی به آرایهٔ session تغییر کرد قرمز مانده بودند. با قرارداد واقعی همخوان شدند؛ سوئیت از ۴ شکست به ۱ رسید |
+| ی.۳ | `lib/getStateInfo.test.js` یک شکست | ⚠️ | «host ناشناخته → بدون تطبیق» با timeout ۵ ثانیه. **در baseline هم بود** و به multi-domain مربوط است نه این تسک. بدهی ثبتشده |
+| ی.۴ | جریان رزرو **هیچ پشتیبانی دارکمود ندارد** | ⚠️ | `darkMode: "class"` در Tailwind ولی `attribute="data-"` در provider عمومی؛ صفر `dark:` در `components/appointment/`. مسئلهٔ پروژهای، نه این تسک |
+| ی.۵ | `ButtonData.js` عمداً کامنت شده | ⚠️ | کامیت `4651ae5`. هر افزودنِ اکشن به پنل کاربر باید جای دیگری برود و تصمیم محصول بگیرد |
diff --git a/tests/Appointment/PublicSiteAppointmentContractTest.php b/tests/Appointment/PublicSiteAppointmentContractTest.php
new file mode 100644
index 00000000..f7fd2503
--- /dev/null
+++ b/tests/Appointment/PublicSiteAppointmentContractTest.php
@@ -0,0 +1,162 @@
+createUser(['ROLE_DOCTOR']);
+ $doctor = new Doctor($owner, 'دکتر قرارداد سایت');
+ $this->em->persist($doctor);
+ $this->em->flush();
+
+ $schedule = $this->newWeeklySchedule($doctor, [
+ '0' => ['sessions' => [[
+ 'active' => true, 'start_time' => '09:00', 'end_time' => '18:00',
+ 'duration_per_patient' => 20, 'location_id' => 1,
+ ]]],
+ ]);
+ $schedule->setMeta(['booking_mode' => WeeklySchedule::MODE_SERVICE, 'buffer_minutes' => $buffer]);
+ $this->em->persist($schedule);
+
+ $section = new ServiceSection('doctor', $doctor->getId(), 'زیبایی');
+ $this->em->persist($section);
+ $this->em->flush();
+
+ return [$owner, $doctor, $section];
+ }
+
+ private function service(ServiceSection $section, string $name, int $minutes): ServiceItem
+ {
+ $item = new ServiceItem($section, $name, 1200000);
+ $item->setDurationMinutes($minutes)->setBookable(true);
+ $this->em->persist($item);
+ $this->em->flush();
+
+ return $item;
+ }
+
+ /** @return array{0:\App\Auth\Entity\User,1:Appointment} */
+ private function bookedFor(Doctor $doctor, array $items, bool $reserve = false): array
+ {
+ $patient = $this->createUser(['ROLE_USER']);
+ $start = (int) strtotime('+3 days 10:00');
+ $appt = $this->newAppointment($doctor, $patient, $start, $start + 35 * 60);
+ $appt->transitionTo(Appointment::STATUS_CONFIRMED);
+ if ($reserve) {
+ $appt->rescheduleTo($start, $start, true);
+ }
+ if ($items !== []) {
+ $appt->replaceServiceItems($items);
+ $appt->setServiceDuration(35, 10);
+ }
+ $this->em->persist($appt);
+ $this->em->flush();
+
+ return [$patient, $appt];
+ }
+
+ /** پاسخ این endpoint دو لایه است: `data.data[]`. */
+ private function rows(\App\Auth\Entity\User $patient): array
+ {
+ $res = $this->authJson('GET', '/api/v1/appointments/user', $patient);
+ self::assertSame(200, $this->responseCode());
+
+ return $res['data']['data'];
+ }
+
+ // ── ✅ موفق ──────────────────────────────────────────────────────────────
+
+ public function testServiceFieldsThePublicPanelNeedsArePresent(): void
+ {
+ [, $doctor, $section] = $this->serviceDoctor();
+ $face = $this->service($section, 'لیزر صورت', 20);
+ $bikini = $this->service($section, 'لیزر بیکینی', 15);
+ [$patient] = $this->bookedFor($doctor, [$face, $bikini]);
+
+ $row = $this->rows($patient)[0];
+
+ self::assertArrayHasKey('service_items', $row);
+ self::assertCount(2, $row['service_items'], 'فهرست کامل، نه فقط سرویس اول');
+ self::assertSame(['لیزر صورت', 'لیزر بیکینی'], array_column($row['service_items'], 'name'));
+
+ self::assertSame(35, $row['service_total_minutes']);
+ self::assertSame(10, $row['service_buffer_minutes']);
+ self::assertArrayHasKey('clinic_uuid', $row);
+ self::assertNull($row['clinic_uuid'], 'مطب شخصی → null');
+ }
+
+ public function testReserveEntryKeepsItsServicesAndDuration(): void
+ {
+ [, $doctor, $section] = $this->serviceDoctor();
+ $item = $this->service($section, 'لیزر', 45);
+ [$patient] = $this->bookedFor($doctor, [$item], reserve: true);
+
+ $row = $this->rows($patient)[0];
+
+ self::assertTrue($row['is_reserve']);
+ self::assertCount(1, $row['service_items']);
+ self::assertSame(35, $row['service_total_minutes'], 'مدت برای تبدیل بعدی میماند');
+ }
+
+ // ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
+
+ public function testSlotModeAppointmentLeavesTheServiceFieldsNull(): void
+ {
+ $owner = $this->createUser(['ROLE_DOCTOR']);
+ $doctor = new Doctor($owner, 'دکتر اسلاتی');
+ $this->em->persist($doctor);
+ $this->em->flush();
+ [$patient] = $this->bookedFor($doctor, []);
+
+ $row = $this->rows($patient)[0];
+
+ // پنل سایت روی این با شرط && میخواند؛ null باید null بماند نه صفر.
+ self::assertNull($row['service_total_minutes']);
+ self::assertNull($row['service_buffer_minutes']);
+ self::assertSame([], $row['service_items']);
+ }
+
+ public function testAppointmentWithoutServicesReturnsAnEmptyArrayNotNull(): void
+ {
+ [, $doctor] = $this->serviceDoctor();
+ [$patient] = $this->bookedFor($doctor, []);
+
+ $row = $this->rows($patient)[0];
+
+ self::assertIsArray($row['service_items'], 'سایت روی length میخواند؛ null کرش میدهد');
+ self::assertSame([], $row['service_items']);
+ }
+
+ public function testEachServiceItemCarriesTheKeysTheSiteReads(): void
+ {
+ [, $doctor, $section] = $this->serviceDoctor();
+ $item = $this->service($section, 'لیزر', 30);
+ [$patient] = $this->bookedFor($doctor, [$item]);
+
+ $first = $this->rows($patient)[0]['service_items'][0];
+
+ foreach (['uuid', 'name', 'price_rials'] as $key) {
+ self::assertArrayHasKey($key, $first);
+ }
+ }
+}