# معماری — تسک ۰۰ب

پروژه: `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` سالم بماند
