# معماری — تسک ۰۰ب پروژه: `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 // وضعیت فعلی

۱. انتخاب سرویس

className={active ? "border-[#5559CE] bg-[#5559CE]/5" : "border-gray-200 bg-white hover:border-[#5559CE]"} ``` ```jsx // هدف — همان ساختار DOM، رنگ از تم

۱. انتخاب سرویس

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 مدت تقریبی: {minutes} دقیقه // پیش از انتخاب روز مدت نوبت: {minutes} دقیقه // پس از انتخاب روز، از همان پاسخ ``` `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 && ( {turn.service_items.map((s) => s.name).join("، ")} )} {turn.service_total_minutes && ( {turn.service_total_minutes} دقیقه )} ``` شرط `&&` اجباری است: نوبت اسلاتی این دو را ندارد و کارتش باید **دقیقاً** مثل امروز بماند. نوبت سرویسیِ قدیمی هم ممکن است `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` سالم بماند