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); + } + } +}