# بازنویسی صفحه‌ی گرفتن نوبت (`/appointment/[doctorId]`) برای هم‌خوانی با API واقعی

## پروژه

`nobat724_front` — سایت عمومی نوبت‌دهی. این کار **صرفاً frontend** است؛ backend (`clinicpro`) کامل و درست است و **تغییر نمی‌کند**. مشکل این است که کل فلوی نوبت‌گیری روی یک قرارداد API **خیالی/قدیمی** نوشته شده که با endpointهای واقعی `clinicpro` هم‌خوان نیست. همه‌ی endpointهای لازم از قبل در `clinicpro/docs/api/appointment.md` و `clinicpro/docs/api/payment.md` مستند و آماده‌اند.

## زمینه

صفحه‌ی `/appointment/[doctorId]` یک wizard چندمرحله‌ای است:

```
step 0: انتخاب تاریخ + ساعت (Date) → step 1/2: لاگین OTP → step 3: تکمیل اطلاعات (Detail) → step 4: پرداخت (Paying) → step 5/6: success/failed
```

این فلو در ظاهر کامل است ولی عملاً کار نمی‌کند چون **هر چهار فراخوانی اصلی API (اسلات‌ها، رزرو، پروفایل، پرداخت) با قرارداد واقعی backend مغایرت دارند** و علاوه بر آن صفحه از یک endpoint که اصلاً وجود ندارد (`appointment/not-available/{id}`) استفاده می‌کند.

## مشکل / هدف

صفحه‌ی نوبت‌گیری را به API واقعی وصل کن: استخراج درست پاسخ‌ها، پارامترها و بدنه‌های درست، و مپ‌کردن ساختار واقعی اسلات (`sessions[].slots[]`) به UI موجود. هدف این است که یک کاربر واقعی بتواند تاریخ/ساعت را ببیند، اسلات انتخاب کند، نوبت رزرو کند و به درگاه پرداخت برود.

این کار باید **مرحله‌به‌مرحله** انجام شود (یک قابلیت در هر مرحله: پیاده‌سازی → build → تأیید → commit).

## قرارداد واقعی Backend (تأییدشده با curl روی `https://clinic-pro.ddev.site`)

### ۱. اسلات‌ها — `GET /api/v1/appointment-slots?doctor_uuid={uuid}&date={Y-m-d}` (PUBLIC)

- پارامتر `doctor_uuid` (UUID) است، **نه** `doctor_id`. فراخوانی با `doctor_id` خطای `ERR_VALIDATION_002` («دکتر یافت نشد») می‌دهد.
- پاسخ:

```json
{
  "success": true,
  "data": {
    "doctor_uuid": "...",
    "date": "2026-06-20",
    "sessions": [
      {
        "start_time": "09:00",
        "end_time": "13:00",
        "slots": [
          { "start": 1781933400, "end": 1781934600, "start_time": "09:00", "end_time": "09:20", "location_id": 2581, "is_available": true }
        ]
      },
      { "start_time": "15:00", "end_time": "17:00", "slots": [ ... ] }
    ]
  }
}
```

> هیچ کلید `morning`/`evening`/`hours_morning`/`hours_afternoon` وجود ندارد. اسلات کلید `time`/`status` ندارد؛ به‌جایش `start_time` (رشته‌ی HH:MM)، `is_available` (boolean)، و `start`/`end` (Unix timestamp) دارد. شیفت‌ها در آرایه‌ی `sessions` هستند (نه دو دسته‌ی ثابت صبح/عصر).

### ۲. رزرو نوبت — `POST /api/v1/appointment` (AUTH)

بدنه:

```json
{ "doctor_uuid": "...", "slot_start": 1781933400, "slot_end": 1781934600, "note": "" }
```

- `doctor_uuid` (نه `doctor_id`)، `slot_start`/`slot_end` به‌صورت Unix timestamp (همان `start`/`end` اسلات)، `note` اختیاری (نه `info`، نه شیء `slot`).
- پاسخ `201`: `{ success, data: { uuid, status: "pending", price, ... } }` — شناسه‌ی نوبت `data.uuid` است (UUID)، **نه** `data.id`.

### ۳. پرداخت — `POST /api/v1/payment/appointment` (AUTH، باید مالک نوبت باشد)

بدنه:

```json
{ "appointment_uuid": "...", "gateway": "mellat", "frontend_address": "https://.../payment/result" }
```

- مسیر `payment/appointment` است، **نه** `payment`. `gateway` فقط `"mellat"` یا `"sep"`. فیلدهای `payment_method`/`bundle`/`entity_reference_id` وجود ندارند.
- پاسخ `200`: `{ success, data: { payment_uuid, redirect_url, order_id } }` — برای ریدایرکت از `data.redirect_url` استفاده کن (نه ساختن دستی URL).

### ۴. پروفایل کاربر — `GET /api/v1/user-profile/{uuid}` (AUTH)

پاسخ `{ success, data: {...} }`.

### ۵. interceptor مهم (`services/api.js`)

```js
api.interceptors.response.use((response) => response.data, ...)
```

همه‌ی `request.*` **یک‌بار** پاسخ را باز می‌کنند و **بدنه‌ی HTTP** (`{ success, data }`) را برمی‌گردانند. پس داده‌ی واقعی همیشه در `result.data` است. (نکته: تابع server-side `fetchReq` در `lib/req.js` هم `response.data` را برمی‌گرداند؛ ولی صفحه‌ی نوبت با `axiosInstance.get(...)` مستقیم کار می‌کند که باز نمی‌کند → آنجا `.data` لازم است.)

> هشدار double-nesting: پاسخ `GET /api/v1/doctor/{uuid}` سه‌لایه است (`{ success, data: { data: {...} } }`). در `axiosInstance` خام یعنی `res.data.data.data`. (این الگو قبلاً در `app/doctor/[slug]/page.js` با `res.data?.data?.data` رفع شده — همان‌جا را مرجع بگیر.)

## فایل‌های مرتبط

| فایل | نقش | مشکل |
|------|-----|------|
| `app/appointment/[doctorId]/page.js` | server: doctor + disabledDates | `doctor = doctorRes.data` (باید `?.data?.data`)؛ فراخوانی endpoint ناموجود `appointment/not-available/{id}` |
| `services/response.js` | لایه‌ی `request.*` | `getAppointment` با `doctor_id`؛ `postAppointment`/`postPayment`/`getPayment` با مسیر/بدنه‌ی غلط؛ `getAppointmentNotAvailable` به route ناموجود |
| `app/component/date/dateTime/index.js` | fetch اسلات‌ها + آدرس | `getAppointment(doctor.id, date)`؛ خواندن `res.morning`/`res.evening`؛ `res.address` |
| `app/component/date/dateTime/hours/List.js` | grid اسلات‌ها | `appo[value ? "evening" : "morning"]`، `item.time`، `item.status` — هیچ‌کدام در API نیست |
| `app/component/date/dateTime/SendAppo.js` | دکمه‌ی «تایید نوبت» | فقط `setSelectedSlot(hour)` — وابسته به شکل `hour` |
| `components/appointment/Content.js` (در `components/appointment/index.js`) | fetch پروفایل کاربر | خواندن `res.uuid`/`res.name` (باید `res.data.*`) |
| `components/appointment/detail/SubmitData.js` | PATCH/POST پروفایل + POST نوبت | بدنه‌ی نوبت غلط؛ خواندن `appointmentResponse.id` (باید `data.uuid`) |
| `components/appointment/paying/index.js` | POST پرداخت + ریدایرکت | بدنه/مسیر غلط؛ خواندن `response.uuid`؛ ساختن دستی URL درگاه |
| `clinicpro/docs/api/appointment.md` | قرارداد اسلات/رزرو | مرجع — تغییر نمی‌کند |
| `clinicpro/docs/api/payment.md` | قرارداد پرداخت | مرجع — تغییر نمی‌کند |

## وضعیت فعلی (کد واقعی مشکل‌دار)

### `app/appointment/[doctorId]/page.js`

```js
const doctorRes = await axiosInstance.get(`${API_URL}/api/v1/doctor/${doctorId}`);
doctor = doctorRes.data;                                  // ❌ باید doctorRes.data?.data?.data

if (doctor && doctor.id) {                                // ❌ doctor.id همیشه undefined
  const disabledDatesRes = await axiosInstance.get(
    `${API_URL}/api/v1/appointment/not-available/${doctor.id}`   // ❌ route وجود ندارد (404)
  );
  disabledDates = disabledDatesRes.data?.data || [];
}
```

### `services/response.js`

```js
getAppointmentNotAvailable: (doctor_id) =>
  api.get(`api/v1/appointment/not-available/${doctor_id}`, removeTokenHead),   // ❌ route ناموجود
getAppointment: (doctor_id, date) =>
  api.get(`api/v1/appointment-slots?date=${date}&doctor_id=${doctor_id}`, removeTokenHead),  // ❌ doctor_id
postAppointment: (data) => api.post(`api/v1/appointment`, data, { requireAuth: true }),
postPayment: (data) => api.post(`api/v1/payment`, data, { requireAuth: true }),              // ❌ مسیر payment/appointment
getPayment: (uuid) => api.get(`api/v1/payment/${uuid}`, { requireAuth: true }),
```

### `app/component/date/dateTime/index.js`

```js
request.getAppointment(doctor.id, date).then((res) => {
  setAppo(res);                                                    // ❌ res، نه res.data
  const hasAvailableMorning = res.morning?.some(s => s.status === "available");   // ❌ morning/status نیست
  const hasAvailableEvening = res.evening?.some(s => s.status === "available");
  ...
});
// آدرس:
request.getDoctorAddress(hour.location_id).then((res) => setLocationAddress(res.address));  // ❌ res.data.address
```

### `app/component/date/dateTime/hours/List.js`

```js
const list = appo && appo[value ? "evening" : "morning"];   // ❌ کلید وجود ندارد
// ...
disabled={item.status !== "available"}                      // ❌ is_available
{item.time}                                                 // ❌ start_time
```

### `components/appointment/detail/SubmitData.js`

```js
const appointmentPayload = {
  doctor_id: doctor.id,                                     // ❌ doctor_uuid
  slot: {                                                   // ❌ backend شیء slot نمی‌خواهد
    time: selectedSlot.time, status: selectedSlot.status,
    start_time_timestamp: selectedSlot.start_time_timestamp,
    end_time_timestamp: selectedSlot.end_time_timestamp,
    duration_per_patient: selectedSlot.duration_per_patient,
    location_id: selectedSlot.location_id
  },
  info: ""                                                  // ❌ note
};
const appointmentResponse = await request.postAppointment(appointmentPayload);
if (appointmentResponse?.id) setAppointmentId(appointmentResponse.id);   // ❌ data.uuid
```

### `components/appointment/paying/index.js`

```js
const paymentPayload = {
  payment_method: selectedBank,                             // ❌ gateway
  bundle: "appointment",                                    // ❌ حذف
  entity_reference_id: appointmentId                        // ❌ appointment_uuid
};
const response = await request.postPayment(paymentPayload);
if (response?.uuid) {                                        // ❌ data.redirect_url
  const paymentUrl = `${process.env.NEXT_PUBLIC_API_URL}/payment/${response.uuid}`;  // ❌ از redirect_url استفاده کن
  window.location.href = paymentUrl;
}
```

### `components/appointment/index.js` (خواندن پروفایل)

```js
const res = await request.getUserProfile(parsedData.uuid);
if (res) {
  const newData = { uuid: res.uuid, ... national_code: { value: res.national_code ... } };  // ❌ res.data.*
}
```

## وظایف

اجرای مرحله‌به‌مرحله؛ بعد از هر قابلیت `npm run build` و سپس commit جدا.

### ۱. اصلاح لایه‌ی `request.*` در `services/response.js`

- `getAppointment` را با `doctor_uuid` بازنویسی کن (صفحه با `[doctorId]` کار می‌کند که همان uuid است):

```js
getAppointmentSlots: (doctor_uuid, date) =>
  api.get(`api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}`, removeTokenHead),
```

- `postAppointment` بدنه‌اش از caller می‌آید (در وظیفه ۴ اصلاح می‌شود) — تغییری در امضای تابع لازم نیست.
- پرداخت را به مسیر و نام درست ببر:

```js
postAppointmentPayment: (data) =>
  api.post(`api/v1/payment/appointment`, data, { requireAuth: true }),
```

- `getAppointmentNotAvailable` و تابع/route ناموجود `not-available` را حذف کن (در وظیفه ۲ مصرفش هم حذف می‌شود).
- اسم‌های قدیمی (`getAppointment`, `postPayment`) را اگر جای دیگری مصرف نمی‌شوند حذف کن؛ اگر مصرف می‌شوند، همه‌ی callerها را به نسخه‌ی جدید مهاجرت بده. (با grep بررسی کن.)

### ۲. اصلاح `app/appointment/[doctorId]/page.js`

- استخراج درست doctor: `doctor = doctorRes.data?.data?.data;`
- منطق `disabledDates` و فراخوانی `appointment/not-available/{id}` را **حذف کن** (route وجود ندارد). prop `disabledDates` را یا حذف کن یا `[]` بفرست تا کامپوننت‌های پایین‌دستی نشکنند. تاریخ‌های غیرفعال در همان پاسخ `appointment-slots` (نبودِ session/اسلات available) منعکس می‌شود؛ منطق غیرفعال‌سازی روز را به آن واگذار کن.
- `disabledDates` در `Time`/`SelectDatePicker` مصرف می‌شود — بررسی کن با آرایه‌ی خالی رفتار درستی دارد (همه‌ی روزها قابل‌انتخاب) و crash نمی‌کند.

### ۳. مپ‌کردن ساختار واقعی اسلات به UI — `dateTime/index.js` + `hours/List.js`

- در `dateTime/index.js`: `request.getAppointmentSlots(doctor.uuid, date)` را صدا بزن (نه `doctor.id`؛ پس از وظیفه ۲، `doctor` شیء واقعی است و `uuid` دارد).
- پاسخ در `res.data` است (interceptor). یک adapter بنویس که `data.sessions[]` را به ساختاری که UI می‌خواهد تبدیل کند. **توهم‌سازی صبح/عصر نکن**؛ یا مستقیم روی `sessions` رندر کن، یا اگر می‌خواهی UI تب‌دار صبح/عصر را نگه داری، sessionها را بر اساس `start_time < "12:00"` به صبح/عصر تقسیم کن و این تصمیم را در adapter مستندِ کوچک بگذار.
- در `List.js`: به‌جای `item.time` از `item.start_time`، به‌جای `item.status !== "available"` از `!item.is_available`، و به‌جای کلید `morning`/`evening` از خروجی adapter استفاده کن.
- `setSelectedSlot(hour)` در `SendAppo` باید شیئی نگه دارد که `start` و `end` (Unix) و `location_id` دارد — همان آبجکت اسلات از API. مطمئن شو این فیلدها تا `SubmitData` می‌رسند.
- بخش آدرس: `request.getDoctorAddress(hour.location_id)` پاسخش `res.data` است — `setLocationAddress(res.data?.address)`. (شکل واقعی `getDoctorAddress` را با یک فراخوانی تأیید کن.)

### ۴. اصلاح ساخت نوبت — `components/appointment/detail/SubmitData.js`

- بدنه‌ی درست:

```js
const appointmentPayload = {
  doctor_uuid: doctor.uuid,
  slot_start: selectedSlot.start,
  slot_end: selectedSlot.end,
  note: "",
};
const res = await request.postAppointment(appointmentPayload);
const appointmentUuid = res?.data?.uuid;
if (appointmentUuid) setAppointmentId(appointmentUuid);   // نام state را appointmentUuid نگه‌دار یا همان appointmentId با مقدار uuid
```

- خطای `409` (اسلات قبلاً رزرو شده، `ERR_CONFLICT_001`) را به پیام فارسی مناسب map کن و کاربر را به مرحله‌ی انتخاب ساعت برگردان.
- بخش PATCH/POST پروفایل قبل از رزرو دست‌نخورده می‌ماند مگر اینکه شکل پاسخ را بشکند — فقط مطمئن شو با interceptor (`res.data`) هم‌خوان است.

### ۵. اصلاح پرداخت — `components/appointment/paying/index.js`

- بدنه و مسیر درست:

```js
const paymentPayload = {
  appointment_uuid: appointmentId,            // همان uuid نوبت
  gateway: selectedBank,                      // فقط "mellat" یا "sep"
  frontend_address: `${window.location.origin}/payment/result`,
};
const res = await request.postAppointmentPayment(paymentPayload);
const redirectUrl = res?.data?.redirect_url;
if (redirectUrl) {
  window.location.href = redirectUrl;         // از redirect_url خود backend استفاده کن
} else {
  setStep((prev) => prev + 1);
}
```

- لیست بانک‌ها (`banks`) را با `gateway`های واقعی هم‌خوان کن: `{ id: "mellat", title: "بانک ملت" }`، `{ id: "sep", title: "سامان (سپ)" }`. مقدار `id` باید دقیقاً `mellat`/`sep` باشد.
- مبلغ hard-code شده‌ی «10،000 تومان» را یا از `price` پاسخ نوبت (`data.price`، ریال) بگیر و با جداکننده‌ی فارسی به ریال/تومان نمایش بده، یا اگر در این مرحله در دسترس نیست، متن مبلغ ثابت را حذف کن (توهم‌سازی مبلغ ممنوع). `price` در پاسخ `POST /api/v1/appointment` هست — می‌توان آن را تا این مرحله پاس داد.

### ۶. اصلاح خواندن پروفایل — `components/appointment/index.js`

- در هر دو `useEffect`، `const res = await request.getUserProfile(parsedData.uuid)` پاسخش `{ success, data }` است → از `res.data` بخوان: `res.data.uuid`, `res.data.national_code`, `res.data.name`, ... . منطق 404 (پروفایل ناموجود → فیلدها قابل‌ویرایش) حفظ شود.

## نکات مهم

- **هیچ تغییری در backend لازم نیست.** اگر به endpoint غایبی برخوردی، **متوقف شو و بپرس** (cross-repo) — داده‌ی جعلی یا route حدسی جایگزین نکن. مشخصاً `appointment/not-available` وجود ندارد و نباید بازسازی شود.
- **interceptor (`services/api.js`):** همه‌ی `request.*` بدنه‌ی `{ success, data }` را برمی‌گردانند → داده در `result.data`. صفحه‌ی server-side با `axiosInstance` خام، doctor را سه‌لایه می‌گیرد (`res.data.data.data`).
- **slug = uuid:** پارامتر route `[doctorId]` در عمل uuid پزشک است؛ همان را به `doctor_uuid` بده، نه `doctor.id` عددی.
- **timestampها Unix هستند:** `slot.start`/`slot.end` مستقیم به‌عنوان `slot_start`/`slot_end` می‌روند؛ تبدیل اضافه نکن.
- **adapter جدا:** برای اسلات‌ها یک تابع map کوچک بنویس (`sessions[]` → ساختار UI) تا کامپوننت نمایشی کم‌تغییر بماند و دیف کوچک شود.
- **Auth:** رزرو و پرداخت `requireAuth` می‌خواهند؛ در 401، `api.js` خودکار logout/redirect می‌کند — این رفتار را حفظ کن. فلوی OTP (step 1/2) دست‌نخورده می‌ماند.
- **Multi-domain/RTL/Jalali:** تاریخ‌ها شمسی، مبالغ ریال با جداکننده‌ی فارسی، `matchedCity` موجود حفظ شود.
- **تست:** بعد از هر قابلیت `npm run build` (ESLint در این پروژه setup نشده؛ build خود type-check را انجام می‌دهد). در صورت امکان فلو را با یک پزشک واقعی (`4a0594b1-008b-478a-a593-259b95d8c2dd`) و تاریخ آینده دستی تست کن: اسلات‌ها باید لود شوند. سپس commit با پیام توصیفی برای همان قابلیت.
- **عدم رگرسیون:** `getAppointment`/`postPayment` قدیمی ممکن است جای دیگری مصرف شوند؛ قبل از حذف/تغییر امضا، با `grep -rn "getAppointment\|postPayment\|getAppointmentNotAvailable" app components services` همه‌ی callerها را پیدا و مهاجرت بده.
