feat: implement service mode completion for nobat724_front

- Add task for completing service mode in clinicpro with detailed objectives and acceptance criteria.
- Create architecture documentation for task 00b, outlining involved components and necessary changes.
- Develop checklist for task 00b to ensure all requirements are met.
- Document implementation notes for task 00b, emphasizing API contract checks and design system adherence.
- Update task documentation for task 00b, specifying goals and current issues with service mode.
This commit is contained in:
hamed
2026-07-30 11:56:08 +03:30
parent 021d0eb6b2
commit 158dcb58aa
12 changed files with 1846 additions and 0 deletions
@@ -0,0 +1,203 @@
# معماری — تسک ۰۰ب
پروژه: `nobat724_front` · Next.js 15 App Router · MUI v5 + Tailwind · RTL · Vazir
## فایل‌های درگیر
```
components/appointment/
├── index.js # ارکستراتور مراحل — تغییر جزئی
├── service/index.js # ⚠️ بازنویسی با توکن تم
├── date/index.js # مصرف adaptServiceSlots
└── detail/SubmitData.js # تغییر جزئی
lib/appointmentSlots.js # adaptServiceSlots شیفت‌آگاه
services/response.js # endpoint های جدید تسک ۰۰
components/dashboard/userAccount/sidebars/turns/
├── Card.js # + سرویس و مدت
├── isTurnsDetails/DetailLg.js
├── isTurnsDetails/DetailSm.js
└── isTurnsDetails/ButtonData.js # + جابه‌جایی سرویس‌آگاه
```
## ۱. بازنویسی `service/index.js` — توکن، نه hex
وضعیت فعلی چهار رنگ hard-code دارد و در دارک‌مود می‌شکند:
```jsx
// وضعیت فعلی
<h2 className="text-[16px] font-bold text-[#3B3B3B] mb-4">۱. انتخاب سرویس</h2>
className={active ? "border-[#5559CE] bg-[#5559CE]/5"
: "border-gray-200 bg-white hover:border-[#5559CE]"}
```
```jsx
// هدف — همان ساختار DOM، رنگ از تم
<h2 className="text-base font-bold text-foreground mb-4">۱. انتخاب سرویس</h2>
className={active
? "border-primary bg-primary/5"
: "border-border bg-surface hover:border-primary"}
```
⚠️ **نام دقیق کلاس‌ها را از `tailwind.config.js` و `mui/index.js` همین پروژه بردار.**
اسم‌های بالا نمونه‌اند. قاعده: هر رنگی که در بقیهٔ مراحل رزرو (`location/`، `date/`،
`information/`) استفاده می‌شود، اینجا هم همان — نه یک پالت جدید.
**رفتار عوض نمی‌شود:** همان toggle، همان ساختار، همان متن‌ها. فقط منبع رنگ.
اگر پروژه توکن معادل ندارد (مثلاً `bg-surface` تعریف نشده)، از همان الگویی استفاده کن
که مرحلهٔ قبلی (`location/index.js`) دارد — نه ساختن توکن جدید در این تسک.
## ۲. حذف محاسبهٔ موازی مدت
```js
// ❌ وضعیت فعلی — منبع دوم حقیقت
const totalMinutes = services
.filter((s) => draft.includes(s.uuid))
.reduce((sum, s) => sum + (Number(s.duration_minutes) || 0), 0);
```
مدت باید از پاسخ `appointment-service-slots` بیاید که از قبل `total_duration_minutes` و
`buffer_minutes` دارد. ولی یک مسئلهٔ ترتیبی هست: مرحلهٔ انتخاب سرویس **پیش از** انتخاب
روز است، و آن endpoint تاریخ می‌خواهد.
دو گزینه:
| گزینه | ارزیابی |
|---|---|
| فراخوانی `appointment-service-slots` با تاریخ امروز فقط برای گرفتن مدت | یک درخواست اضافه، و اگر امروز تعطیل باشد پاسخ خالی است ولی `total_duration_minutes` همچنان می‌آید ✅ |
| نگه‌داشتن محاسبهٔ فرانت به‌عنوان تخمین + اصلاح در مرحلهٔ بعد | بیمار دو عدد متفاوت می‌بیند ❌ |
**انتخاب: گزینهٔ اول**، با یک تفاوت مهم — مدت **تخمینی** برچسب می‌گیرد تا وقتی روز
انتخاب نشده:
```js
// مرحلهٔ انتخاب سرویس
const { data } = useServiceDuration(doctorUuid, clinicUuid, draft); // hook جدید
const minutes = data?.total_duration_minutes ?? fallbackSum(draft); // fallback با console.warn
<span>مدت تقریبی: {minutes} دقیقه</span> // پیش از انتخاب روز
<span>مدت نوبت: {minutes} دقیقه</span> // پس از انتخاب روز، از همان پاسخ
```
`fallbackSum` فقط برای بک‌اند قدیمی است و `console.warn` می‌زند. حذفش پس از deploy تسک ۰۰.
## ۳. `adaptServiceSlots` شیفت‌آگاه
مشکل: همهٔ زمان‌ها در یک تب با برچسب ثابت «زمان‌های خالی» جمع می‌شوند و `end_time`
اشتباه است (پایانِ آخرین **شروع**، نه پایان نوبت).
```js
// هدف — گروه‌بندی بر اساس شکاف زمانی، با برچسب واقعی
export function adaptServiceSlots(slotsResponse) {
const payload = slotsResponse?.data ?? slotsResponse ?? {};
const starts = payload.start_times ?? [];
if (!starts.length) return [];
const durationMin = Number(payload.total_duration_minutes) || 0;
const GAP_THRESHOLD_MIN = 60; // شکاف بیشتر از یک ساعت = شیفت جدا
const groups = [];
let current = null;
for (const s of starts) {
const gapMin = current
? (s.start - current.slots[current.slots.length - 1].start) / 60
: Infinity;
if (!current || gapMin > GAP_THRESHOLD_MIN) {
current = { slots: [] };
groups.push(current);
}
current.slots.push({ ...s, is_available: true });
}
return groups.map((g) => {
const first = g.slots[0];
const last = g.slots[g.slots.length - 1];
const endTime = last.end_time ?? addMinutes(last.start_time, durationMin);
return {
start_time: first.start_time,
end_time: endTime,
label: `${first.start_time} - ${endTime}`, // ← همان قالب حالت اسلاتی
slots: g.slots,
};
});
}
```
`end_time` هر start از قبل در پاسخ بک‌اند هست (`getServiceStartTimes` هر آیتم را با
`end_time` می‌دهد) — پس `addMinutes` فقط fallback است.
**چرا آستانهٔ شکاف و نه اطلاعات شیفت از بک‌اند؟** پاسخ `appointment-service-slots`
امروز فقط `start_times` مسطح می‌دهد و شیفت را نمی‌گوید. دو راه بود:
| راه | ارزیابی |
|---|---|
| افزودن گروه‌بندی شیفت به پاسخ بک‌اند | درست‌تر، ولی تغییر قرارداد endpoint که سه کلاینت مصرفش می‌کنند — و تسک ۰۰ آن را قفل نکرده ولی بازش هم نکرده |
| **گروه‌بندی هیوریستیک در فرانت** ✅ | بدون تغییر قرارداد؛ برای شیفت صبح/عصر (شکاف معمولاً ۲-۳ ساعت) دقیق است |
انتخاب دوم برای این تسک. اگر بعداً دقت کافی نبود، تسک ۰۶ که `AvailabilityEngine` را
می‌سازد می‌تواند گروه‌بندی واقعی را در پاسخِ **endpoint جدید** بدهد — بدون دست زدن به
این یکی. این تصمیم را در `docs/api/appointment.md` سمت بک‌اند هم یادداشت کن.
`adaptSlots()` (حالت اسلاتی) **یک خط هم** عوض نمی‌شود.
## ۴. سرویس و مدت در پنل کاربر
`GET /api/v1/appointments/user` از قبل چه می‌دهد؟ **پیش از کدنویسی بررسی کن.** اگر
`service_items` و `service_total_minutes` در پاسخ نیست:
- ستون `service_total_minutes` در تسک ۰۰ اضافه شد ✅
- افزودنش به سریالایزر پاسخ، **بخشی از تسک ۰۰** است (ردیف ۱.۱۶ چک‌لیستش)
- اگر جا افتاده، اینجا به‌عنوان یک ردیف ⚠️ ثبت و به تسک ۰۰ برگردان
```jsx
// Card.js — دو خط جدید، فقط وقتی داده هست
{turn.service_items?.length > 0 && (
<span className="…">{turn.service_items.map((s) => s.name).join("، ")}</span>
)}
{turn.service_total_minutes && (
<span className="…">{turn.service_total_minutes} دقیقه</span>
)}
```
شرط `&&` اجباری است: نوبت اسلاتی این دو را ندارد و کارتش باید **دقیقاً** مثل امروز
بماند. نوبت سرویسیِ قدیمی هم ممکن است `service_items` خالی داشته باشد → نام «—».
## ۵. جابه‌جایی سرویس‌آگاه از پنل
`services/response.js` سه متد جدید می‌گیرد:
```js
serviceReschedule: (uuid, body) =>
request.post(`api/v1/appointment/${uuid}/service-reschedule`, body, { requireAuth: true }),
getServiceSlotsForReschedule: (doctor_uuid, date, service_uuids, exclude_uuid, clinic_uuid) =>
request.get(
`api/v1/appointment-service-slots?doctor_uuid=${doctor_uuid}&date=${date}` +
service_uuids.map((u) => `&service_item_uuids[]=${encodeURIComponent(u)}`).join("") +
`&exclude_appointment_uuid=${exclude_uuid}` + clinicQuery(clinic_uuid),
{ requireAuth: true }
),
```
`ButtonData.js` یک دکمهٔ «جابه‌جایی» می‌گیرد که مودال موجود
(`isTurnsDetails/modal/index.js`) را با کامپوننت انتخاب زمان باز می‌کند — همان
`components/appointment/date/` بازاستفاده می‌شود، نه یک انتخابگر جدید.
بیمار **مدت را وارد نمی‌کند**: `service-reschedule` فقط `start` می‌گیرد و مدت را از
سرویس‌های موجود نوبت حساب می‌کند (تسک ۰۰، بخش `ServiceRescheduleService`).
## UI — قواعد اجباری
رجوع: [_shared/ui-conventions.md](../_shared/ui-conventions.md)، بخش `nobat724_front`
- تم MUI از `mui/index.js` — تم جدید نساز
- فونت فقط Vazir از `app/globals.css`
- `darkMode: "class"`؛ صفحات عمومی `data-theme`، پنل `class` — هر دو بررسی شوند
- کامپوننت‌های موجود `components/appointment/*` توسعه داده شوند، مسیر موازی نه
- تاریخ شمسی با `jalali-moment`
- RTL — `ms-*`/`me-*`
- هر صفحه‌ای که دست خورد، `generateMetadata` و `await params` سالم بماند
@@ -0,0 +1,123 @@
# چک‌لیست — تسک ۰۰ب (سازگارسازی nobat724_front)
**وضعیت کلی:** ⏳ شروع نشده
**آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
خط سرخ‌ها: [_shared/red-lines.md](../_shared/red-lines.md) ·
UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ — مسیر اسلاتی سایت
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `adaptSlots()` یک خط هم عوض نشد | ⏳ | |
| ۰.۲ | رندر تب‌های شیفت در حالت اسلاتی دست‌نخورده | ⏳ | |
| ۰.۳ | مسیر رزرو اسلاتی سرتاسر دستی تست شد — بیت‌به‌بیت مثل قبل | ⏳ | سناریو ۳ |
| ۰.۴ | کارت نوبت اسلاتی در پنل بدون تغییر | ⏳ | سناریو ۵ |
## ۱. پیش‌بررسی قرارداد 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` کار می‌کند | ⏳ | تسک ۰۰ ساخته |
## ۲. انتخاب سرویس — دیزاین و منطق
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | چهار رنگ hard-code (`#3B3B3B` `#7A7A7A` `#5559CE` `bg-white`) حذف شد | ⏳ | |
| ۲.۲ | کلاس‌ها از همان الگوی `location/` و `date/` کپی شد، توکن جدید ساخته نشد | ⏳ | |
| ۲.۳ | ساختار DOM و رفتار toggle عوض نشد | ⏳ | فقط منبع رنگ |
| ۲.۴ | محاسبهٔ `reduce` مدت از فرانت حذف شد | ⏳ | |
| ۲.۵ | مدت از `total_duration_minutes` بک‌اند می‌آید | ⏳ | |
| ۲.۶ | `fallbackSum` با `console.warn` — موقت، تسک مقصد حذفش ثبت شد | ⏳ | |
| ۲.۷ | برچسب «مدت تقریبی» پیش از انتخاب روز، «مدت نوبت» پس از آن | ⏳ | |
| ۲.۸ | انتخاب صفر سرویس → دکمهٔ ادامه غیرفعال با راهنمای فارسی | ⏳ | |
| ۲.۹ | محل سرویسی بدون سرویس `bookable` → پیام روشن + پیشنهاد محل دیگر | ⏳ | |
## ۳. `adaptServiceSlots` شیفت‌آگاه
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | گروه‌بندی بر اساس شکاف زمانی پیاده شد | ⏳ | |
| ۳.۲ | آستانه = `max(60, durationMin)` | ⏳ | وگرنه نوبت بلند به تب‌های تک‌عضوی می‌شکند |
| ۳.۳ | برچسب واقعی `"HH:MM - HH:MM"` — نه «زمان‌های خالی» ثابت | ⏳ | |
| ۳.۴ | `end_time` از پاسخ بک‌اند، `addMinutes` فقط fallback | ⏳ | |
| ۳.۵ | کامنت: هیوریستیک است، راه دقیق endpoint تسک ۰۶ | ⏳ | |
| ۳.۶ | `start_times` خالی → `[]` و پیام دلیل‌دار در UI | ⏳ | |
## ۴. پنل کاربر — سرویس، مدت، جابه‌جایی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `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()` اجرا می‌شود | ⏳ | |
## ۵. UI — قواعد سایت
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | تم MUI از `mui/index.js` — تم جدید ساخته نشد | ⏳ | |
| ۵.۲ | فونت فقط Vazir — فونت جدید اضافه نشد | ⏳ | |
| ۵.۳ | دارک‌مود صفحات عمومی (`data-theme`) بررسی شد | ⏳ | سناریو ۲ |
| ۵.۴ | دارک‌مود پنل (`class`) بررسی شد | ⏳ | سناریو ۷ — مکانیزم متفاوت |
| ۵.۵ | کامپوننت موازی ساخته نشد؛ `components/appointment/*` توسعه یافت | ⏳ | |
| ۵.۶ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ⏳ | |
| ۵.۷ | موبایل بررسی شد — بدون اسکرول افقی | ⏳ | سناریو ۱۰ |
| ۵.۸ | تاریخ‌ها شمسی با `jalali-moment` | ⏳ | |
| ۵.۹ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۵.۱۰ | صفحاتی که دست خوردند `generateMetadata` و `await params` سالم دارند | ⏳ | |
| ۵.۱۱ | دامنه گسترش نیافت — صفحهٔ رزرو بازطراحی نشد | ⏳ | انحراف بقیهٔ مراحل، اگر بود، ⚠️ ثبت شود |
## ۶. تست دستی — ده سناریو
| # | سناریو | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | رزرو سرویسی کامل تا پیامک | ⏳ | |
| ۶.۲ | همان در دارک‌مود عمومی | ⏳ | |
| ۶.۳ | رزرو اسلاتی کامل — بدون تغییر | ⏳ | ⛔ خط سرخ |
| ۶.۴ | پزشک دو-شیفته سرویسی → دو تب با برچسب واقعی | ⏳ | |
| ۶.۵ | پنل با نوبت اسلاتی تنها → بدون تغییر | ⏳ | |
| ۶.۶ | پنل با نوبت سرویسی → سرویس و مدت | ⏳ | |
| ۶.۷ | پنل در دارک‌مود | ⏳ | |
| ۶.۸ | جابه‌جایی سرویسی → مدت حفظ | ⏳ | |
| ۶.۹ | جابه‌جایی به زمان اشغال → پیام فارسی + refetch | ⏳ | |
| ۶.۱۰ | همهٔ موارد بالا روی موبایل | ⏳ | |
| ۶.۱۱ | تست واحد `adaptServiceSlots` (پنج حالت) | ⏳ | تابع خالص، بهترین کاندید |
## ۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | `nobat724_front/CLAUDE.md` بخش «حالت‌های نوبت‌دهی» | ⏳ | |
| ۷.۲ | یادداشت هیوریستیک شیفت در `clinicpro/docs/api/appointment.md` | ⏳ | |
## ۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | همهٔ ردیف‌های بالا وضعیت نهایی دارند (هیچ 🔄 و ⏳ بی‌دلیل) | ⏳ | |
| ۸.۲ | `npm run build` بدون خطا | ⏳ | |
| ۸.۳ | `npm run lint` بدون خطای جدید | ⏳ | |
| ۸.۴ | ده سناریوی دستی بخش ۶ اجرا شد | ⏳ | |
| ۸.۵ | چک‌لیست UI (بخش ۵) کامل شد | ⏳ | |
| ۸.۶ | `clinic-pro-tauri` دستی بررسی شد — قرارداد مشترک نشکسته | ⏳ | همان `service_item` تکی |
| ۸.۷ | commit شد، سپس `graphify update .` | ⏳ | |
| ۸.۸ | موارد به‌تعویق‌افتاده با دلیل و تسک مقصد ثبت شدند | ⏳ | `fallbackSum` |
@@ -0,0 +1,157 @@
# نکات پیاده‌سازی — تسک ۰۰ب
## ۱. اول قرارداد پاسخ را بررسی کن، بعد کد بزن
سه چیز را پیش از شروع تأیید کن:
```bash
# ۱. پاسخ appointments/user چه فیلدهایی دارد؟
curl -s -H "Authorization: Bearer $TOKEN" \
https://clinic-pro.ddev.site/api/v1/appointments/user | jq '.data[0] | keys'
# ۲. start_times هر آیتم end_time دارد؟
curl -s "https://clinic-pro.ddev.site/api/v1/appointment-service-slots?doctor_uuid=…&date=…&service_item_uuids[]=…" \
| jq '.data.start_times[0]'
# ۳. total_duration_minutes و buffer_minutes در پاسخ هستند؟
```
اگر `service_items` یا `service_total_minutes` در پاسخ `appointments/user` نیست،
**به تسک ۰۰ برگردان** — سریالایزر آنجا اصلاح می‌شود، نه اینکه اینجا از endpoint دیگری
دور بزنیم.
## ۲. رنگ‌ها را از همسایه کپی کن، نه از حافظه
```bash
# ببین مرحلهٔ قبلی رزرو چه کلاسی می‌زند
grep -n "className" components/appointment/location/index.js | head -30
grep -n "className" components/appointment/date/index.js | head -30
```
هدف: کامپوننت انتخاب سرویس **از بقیهٔ مراحل قابل تشخیص نباشد**. اگر بقیه `bg-white`
می‌زنند و توکن ندارند، تو هم توکن جدید نساز — همان کاری را بکن که آن‌ها می‌کنند،
و اگر دارک‌مود در آن‌ها هم شکسته است، این یک مسئلهٔ جدا است که در چک‌لیست ⚠️ ثبت
می‌شود، نه اینکه در این تسک کل صفحهٔ رزرو بازطراحی شود.
**دامنه را گسترش نده.** فقط `service/index.js` که خودمان اضافه کردیم و از بقیه منحرف است.
## ۳. `fallbackSum` موقت است و باید هشدار بدهد
```js
const minutes = data?.total_duration_minutes ?? (() => {
console.warn('[booking] total_duration_minutes missing — falling back to client sum');
return fallbackSum(draft, services);
})();
```
بدون `console.warn`، بک‌اندی که فیلد را نمی‌دهد بی‌صدا کار می‌کند و شش ماه بعد کسی
نمی‌فهمد چرا مدت با نوبت نمی‌خواند. حذف `fallbackSum` پس از deploy تسک ۰۰ یک ردیف
⏳ در چک‌لیست است با تسک مقصد مشخص.
## ۴. آستانهٔ شکاف: ۶۰ دقیقه، با دلیل
```js
const GAP_THRESHOLD_MIN = 60;
```
شیفت صبح/عصر معمولاً ۲-۳ ساعت فاصله دارد. یک نوبت ۹۰ دقیقه‌ای هم می‌تواند شکاف ۹۰
دقیقه‌ای بسازد بدون اینکه شیفت جدا باشد — پس آستانه نباید کمتر از مدت نوبت باشد:
```js
const threshold = Math.max(GAP_THRESHOLD_MIN, durationMin);
```
این خط را فراموش نکن، وگرنه نوبت‌های بلند به تب‌های تک‌عضوی تقسیم می‌شوند.
هیوریستیک است و در کامنت باید بنویسی: راه دقیق، گروه‌بندی از سمت بک‌اند است که تسک ۰۶
در endpoint جدید می‌دهد.
## ۵. شرط `&&` روی فیلدهای سرویسی در پنل
```jsx
{turn.service_items?.length > 0 && ( )}
```
نه `turn.service_items.map(...)` خالی. نوبت اسلاتی این فیلد را ندارد و بدون شرط، کارت
همهٔ نوبت‌های اسلاتی کرش می‌کند — یعنی کل پنل کاربر می‌شکند، نه فقط یک خط.
تست: پنل کاربری که **فقط** نوبت اسلاتی دارد باید بدون هیچ تغییری رندر شود.
## ۶. جابه‌جایی: مودال موجود، انتخابگر موجود
```
ButtonData.js → دکمهٔ «جابه‌جایی» → isTurnsDetails/modal/index.js
└─ components/appointment/date/ بازاستفاده
```
انتخابگر تاریخ/ساعت جدید نساز. کامپوننت `date/` از قبل هر دو حالت را می‌شناسد
(`adaptSlots` و `adaptServiceSlots`) و همان را با props متفاوت صدا بزن.
## ۷. خطای بک‌اند را نمایش بده، نه پیام عمومی
```js
// ❌
catch { toast.error('خطایی رخ داد'); }
// ✅
catch (err) {
const msg = err?.response?.data?.errors?.[0]?.message ?? 'خطایی رخ داد';
toast.error(msg);
refetchSlots(); // ← فهرست زمان‌ها به‌روز شود
}
```
`ERR_SLOT_TAKEN` پیام فارسی دقیق دارد («این بازه زمانی قبلاً رزرو شده است»). نشان دادن
«خطای نامشخص» یعنی بیمار همان دکمه را ده بار می‌زند. `refetchSlots()` بعد از خطای تداخل
اجباری است.
## ۸. edge case ها
| حالت | رفتار درست |
|---|---|
| محل سرویسی بدون سرویس `bookable` | پیام روشن + پیشنهاد محل دیگر اگر باشد |
| پزشک اسلاتی در مطب، سرویسی در کلینیک | تعویض محل، مرحلهٔ سرویس را ظاهر/پنهان می‌کند و انتخاب‌ها باطل می‌شوند (رفتار موجود `changeLocation`) |
| `start_times` خالی | پیام دلیل‌دار، نه فهرست خالی |
| `total_duration_minutes` غایب | `fallbackSum` + `console.warn` |
| نوبت سرویسی قدیمی بدون `service_items` | نام «—»، مدت اگر هست نمایش، بدون کرش |
| پنل کاربری فقط با نوبت اسلاتی | بیت‌به‌بیت مثل امروز |
| نوبت رزرو (`is_reserve`) در پنل | مدت نمایش داده نشود (زمان ندارد)، فقط سرویس‌ها |
| جابه‌جایی به زمان اشغال‌شده | پیام فارسی بک‌اند + `refetch` |
| دارک‌مود در پنل (`class`) و صفحات عمومی (`data-theme`) | هر دو بررسی شوند — دو مکانیزم متفاوت‌اند |
| یک سرویس با `duration_minutes = null` | بک‌اند `422` می‌دهد؛ UI پیامش را نشان دهد و آن سرویس را برجسته کند |
## ۹. تست
پروژه تست خودکار محدودی دارد. سناریوهای دستی اجباری (در چک‌لیست ثبت شوند):
```
۱. رزرو سرویسی کامل: انتخاب محل سرویسی → سرویس → روز → ساعت → ثبت → پیامک
۲. همان مسیر در دارک‌مود (صفحات عمومی، data-theme)
۳. رزرو اسلاتی کامل — باید بیت‌به‌بیت مثل قبل باشد
۴. پزشک با دو شیفت در حالت سرویسی → دو تب زمانی با برچسب واقعی
۵. پنل کاربر با نوبت اسلاتی تنها → بدون تغییر
۶. پنل کاربر با نوبت سرویسی → سرویس‌ها و مدت دیده می‌شود
۷. پنل کاربر در دارک‌مود (class)
۸. جابه‌جایی نوبت سرویسی → مدت حفظ می‌شود
۹. جابه‌جایی به زمان اشغال‌شده → پیام فارسی + refetch
۱۰. موبایل: هر ده مورد بالا، بدون اسکرول افقی
```
اگر تست خودکار اضافه می‌کنی، `adaptServiceSlots` تابع خالص است و بهترین کاندید:
```
tests/appointmentSlots.test.js
- یک شیفت → یک گروه
- دو شیفت با شکاف ۳ ساعت → دو گروه با برچسب درست
- نوبت ۹۰ دقیقه‌ای با شکاف ۹۰ دقیقه → یک گروه (آستانه = max(60, duration))
- start_times خالی → []
- end_time از پاسخ می‌آید، نه محاسبه
```
## ۱۰. مستندات
`nobat724_front/CLAUDE.md` یک بخش کوتاه «حالت‌های نوبت‌دهی» بگیرد: `slot` و `service`،
اینکه per محل تعیین می‌شوند، و اینکه `adaptSlots`/`adaptServiceSlots` نقطهٔ تفکیک‌اند.
در `clinicpro/docs/api/appointment.md` یادداشت کن که گروه‌بندی شیفت در حالت سرویسی
هیوریستیک سمت فرانت است و راه دقیقش endpoint تسک ۰۶ است.
@@ -0,0 +1,136 @@
# تسک ۰۰ب — سازگارسازی nobat724_front با وضعیت فعلی نوبت‌دهی سرویسی
**پروژه:** `nobat724_front` (سایت عمومی) · **فاز:** ۰ · **وابستگی:** ۰۰ · **زمان:** ۱۰-۱۴ ساعت
**پیش‌نیاز همهٔ تسک‌های ۰۱ به بعد**
---
## ⛔ خط سرخ
مسیر اسلاتی سایت دست‌کاری نمی‌شود: `adaptSlots()`، رندر تب‌های شیفت، و همهٔ رفتار
`booking_mode === 'slot'` عیناً می‌ماند.
رجوع: [_shared/red-lines.md](../_shared/red-lines.md)
---
## هدف
سایت حالت سرویسی را **می‌شناسد** ولی سه دسته مشکل دارد: انحراف از دیزاین‌سیستم،
محاسبهٔ موازی مدت در فرانت، و نبود سرویس/مدت در پنل کاربر. این تسک همه را می‌بندد و
سایت را با endpoint های جدید تسک ۰۰ هم‌گام می‌کند.
## وضعیت فعلی
### ✅ کار می‌کند
| مورد | فایل |
|---|---|
| تشخیص حالت per محل | `components/appointment/index.js:125``selectedLocation?.booking_mode === "service"` |
| مرحلهٔ انتخاب سرویس | `components/appointment/service/index.js` |
| فراخوانی endpoint ها | `services/response.js:78,83` |
| تبدیل پاسخ به قالب اسلات | `lib/appointmentSlots.js``adaptServiceSlots()` |
| ارسال سرویس‌ها در ثبت | `components/appointment/detail/SubmitData.js:152` |
| JSON-LD `availableService` با `estimatedDuration` | `app/doctor/[slug]/page.js:212` |
| باطل‌کردن انتخاب‌ها با تعویض محل | `changeLocation()` در `index.js` |
### ❌ مشکلات این تسک
**۱. انحراف از دیزاین‌سیستم — رنگ‌های hard-code.**
`components/appointment/service/index.js`:
```jsx
<h2 className="text-[16px] font-bold text-[#3B3B3B] mb-4">۱. انتخاب سرویس</h2>
<p className="text-[14px] text-[#7A7A7A]"></p>
className={active
? "border-[#5559CE] bg-[#5559CE]/5"
: "border-gray-200 bg-white hover:border-[#5559CE]"}
```
چهار رنگ hard-code. سایت `darkMode: "class"` دارد و صفحات عمومی با `data-theme` تم
عوض می‌کنند — این کامپوننت در دارک‌مود می‌شکند. بقیهٔ مراحل رزرو از تم MUI/Tailwind
استفاده می‌کنند و این یکی نمی‌کند.
**۲. محاسبهٔ موازی مدت در فرانت.**
```js
// components/appointment/service/index.js
const totalMinutes = services
.filter((s) => draft.includes(s.uuid))
.reduce((sum, s) => sum + (Number(s.duration_minutes) || 0), 0);
```
بک‌اند همان عدد را در `total_duration_minutes` پاسخ `appointment-service-slots`
برمی‌گرداند. دو محاسبه یعنی: وقتی تسک ۰۴ فرمول را به «زمان تنها / زمان اضافه» عوض کند،
سایت عدد قدیمی نشان می‌دهد و بیمار مدتی می‌بیند که با مدت واقعی نوبتش نمی‌خواند.
**۳. `adaptServiceSlots` برچسب گمراه‌کننده می‌سازد.**
```js
return [{
start_time: starts[0].start_time,
end_time: starts[starts.length - 1].start_time, // ← پایانِ آخرین شروع، نه پایان نوبت
label: "زمان‌های خالی",
slots: ,
}];
```
همهٔ زمان‌ها در یک تب جمع می‌شوند و مرز شیفت‌ها (صبح/عصر) از بین می‌رود — در حالی که
حالت اسلاتی همان اطلاعات را از بک‌اند دارد و نشان می‌دهد. برای پزشکی با شیفت صبح و عصر،
بیمار یک فهرست بلند بی‌ساختار می‌بیند.
**۴. پنل کاربر سرویس و مدت نوبت را نشان نمی‌دهد.**
`components/dashboard/userAccount/sidebars/turns/Card.js` و `isTurnsDetails/*` هیچ ارجاعی
به `service` یا مدت ندارند. بیمار نوبت سرویسی گرفته و در پنلش نمی‌بیند چه سرویسی رزرو
کرده یا نوبتش چند دقیقه است.
**۵. جابه‌جایی نوبت در پنل کاربر، سرویس‌آگاه نیست.**
پس از تسک ۰۰، endpoint `POST /appointment/{uuid}/service-reschedule` وجود دارد.
`ButtonData.js` هیچ مسیری برای جابه‌جایی ندارد.
## دامنه
**هست:**
- بازنویسی `components/appointment/service/index.js` با توکن‌های تم (بدون تغییر رفتار)
- حذف محاسبهٔ مدت از فرانت — مصرف `total_duration_minutes` بک‌اند
- `adaptServiceSlots` گروه‌بندی per شیفت
- نمایش سرویس‌ها و مدت در کارت و جزئیات نوبت پنل کاربر
- جابه‌جایی سرویس‌آگاه از پنل کاربر
- به‌روزرسانی `services/response.js` برای endpoint های جدید تسک ۰۰
**نیست:** تغییری در مسیر اسلاتی · حالت `resource` (تسک ۰۶ و پس از آن، یک تسک frontend جدا)
## معیار پذیرش
- ✅ موفق: مرحلهٔ انتخاب سرویس در دارک‌مود درست رندر می‌شود (هیچ متن سیاه روی زمینهٔ
تیره، هیچ کارت سفید).
- ✅ موفق: مدت نمایش‌داده‌شده در مرحلهٔ انتخاب سرویس **از پاسخ بک‌اند** می‌آید؛ اگر
بک‌اند عدد متفاوتی بدهد، UI همان را نشان می‌دهد.
- ✅ موفق: پزشکی با دو شیفت (صبح ۹-۱۳، عصر ۱۶-۲۰) در حالت سرویسی → دو تب زمانی،
با برچسب واقعی هر شیفت.
- ✅ موفق: کارت نوبت در پنل کاربر نام سرویس‌ها و مدت را نشان می‌دهد؛ نوبت اسلاتی
دقیقاً مثل امروز (بدون این دو خط).
- ✅ موفق: بیمار از پنل نوبت سرویسی‌اش را جابه‌جا می‌کند → مدت خودکار حفظ می‌شود،
بیمار عددی وارد نمی‌کند.
- ❌ خطا: جابه‌جایی به زمان اشغال‌شده → پیام فارسی از بک‌اند نمایش داده می‌شود
(نه «خطای نامشخص»)، و فهرست زمان‌ها خودکار به‌روز می‌شود.
- ❌ خطا: انتخاب صفر سرویس → دکمهٔ ادامه غیرفعال با راهنمای فارسی.
- ⚠️ مرزی: محلی که `booking_mode = 'service'` است ولی هیچ سرویس `bookable` ندارد →
پیام روشن («سرویسی برای نوبت‌دهی آنلاین تعریف نشده است») + پیشنهاد محل دیگر اگر باشد.
- ⚠️ مرزی: پزشک در مطب شخصی اسلاتی و در کلینیک سرویسی → تعویض محل، مرحلهٔ سرویس را
ظاهر/پنهان می‌کند و انتخاب‌های قبلی باطل می‌شوند (رفتار موجود، حفظ شود).
- ⚠️ مرزی: پاسخ `appointment-service-slots` خالی → پیام دلیل‌دار، نه فهرست خالی بی‌توضیح.
- ⚠️ مرزی: نوبت قدیمی سرویسی بدون `service_items` → کارت مدت را نشان می‌دهد و نام
سرویس را «—»؛ کرش نمی‌کند.
- ⚠️ مرزی: `total_duration_minutes` در پاسخ نبود (بک‌اند قدیمی) → fallback به محاسبهٔ
فرانت با یک `console.warn`، نه صفحهٔ خالی.
## خروجی
- `components/appointment/service/index.js` بازنویسی‌شده با توکن تم
- `lib/appointmentSlots.js``adaptServiceSlots` شیفت‌آگاه
- `components/dashboard/userAccount/sidebars/turns/*` — سرویس و مدت
- `services/response.js` — endpoint های جدید
- [checklist.md](checklist.md) کامل‌شده