Files
nobat724_front/.claude/prompt/service-based-online-booking.md

108 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نوبت‌دهی آنلاین بر اساس سرویس (Service-first booking)
## پروژه
`nobat724_front` (سایت عمومی نوبت‌دهی).
**Cross-repo:** وابسته به پرامپت backend `clinicpro/.claude/prompt/service-based-booking.md` — آن **اول** اجرا شود؛ این پرامپت endpoint و متای آن را مصرف می‌کند.
## زمینه
جریان فعلیِ نوبت‌گیری آنلاین یک wizard چندمرحله‌ای است بدون انتخاب سرویس:
`/appointment/[doctorId]` → روز → ساعت (اسلات ثابت) → لاگین/OTP → فرم بیمار → پرداخت.
اسلات‌ها از `GET /api/v1/appointment-slots?doctor_uuid&date` می‌آیند و همه هم‌اندازه‌اند.
بک‌اند حالت جدید «نوبت‌دهی بر اساس سرویس» را اضافه می‌کند: مدت نوبت = مجموع مدت سرویس‌های انتخابی، و endpoint جدید زمان‌های خالیِ کافی را برمی‌گرداند. سایت باید در این حالت **اول سرویس** را از بیمار بگیرد، سپس فقط زمان‌های خالیِ کافی را نشان دهد.
## هدف / spec انگلیسی
When a doctor uses `booking_mode = service`, insert a service-selection step **before** the date/time step. The patient picks one or more services; the site fetches candidate start times sized to the summed service duration from the new backend endpoint, shows only those, locks the chosen time via `postAppointment` (with the service items), then continues to the existing login/pay flow. When the doctor is in `slot` mode, the current flow is unchanged. «نوبت آزاد» (reserve) is secretary-only and never shown online.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `services/response.js` (L52 `getAppointmentSlots`, L57 `getMonthAvailability`, L62 `postAppointment`, L76 `getDoctorServices`) | افزودن `getServiceSlots` + رساندن `service_item_uuids` به postAppointment |
| `components/appointment/index.js` (L45 `AppointmentPage`, state L59-62) | افزودن state سرویس‌های انتخابی + مدت |
| `components/appointment/Container.js` (L42 `elements[]` step router) | افزودن مرحلهٔ انتخاب سرویس در ابتدای wizard (حالت سرویس) |
| `app/component/date/dateTime/index.js` (L29-52 fetch اسلات) | در حالت سرویس، فراخوانی endpoint سرویس با مدت مجموع |
| `lib/appointmentSlots.js` (L1 `adaptSlots`, L17 `hasAvailable`) | adapter برای پاسخ `start_times` |
| `components/appointment/detail/SubmitData.js` (L138-165 payload) | افزودن `service_item_uuids` به appointmentPayload |
| `app/appointment/[doctorId]/page.js` (L9, doctor fetch L17) | رساندن `booking_mode` و لیست سرویس‌ها به AppointmentPage |
## وضعیت فعلی (کد واقعی)
### fetch اسلات ثابت
```js
// app/component/date/dateTime/index.js:29
request.getAppointmentSlots(doctor.uuid, dateStr).then((res) => {
const parsed = adaptSlots(res); // data.sessions[].slots
...
});
```
```js
// services/response.js:52
getAppointmentSlots: (doctor_uuid, date) =>
api.get(`api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}`),
```
### payload رزرو — بدون سرویس
```js
// components/appointment/detail/SubmitData.js:138
const appointmentPayload = {
doctor_uuid, slot_start: selectedSlot.start, slot_end: selectedSlot.end,
for_self, patient_national_code, patient_gender, city_id,
}; // هیچ service_id ندارد
request.postAppointment(appointmentPayload); // POST api/v1/appointment
```
### step router
```js
// components/appointment/Container.js:42
const elements = [Date, Login, Verify, Detail, Paying, SuccessPay, FailedPay];
// step: 0 روز/ساعت, 1 لاگین, 2 OTP, 3 فرم, 4 پرداخت ...
```
## وظایف
### ۱. سرویس در response.js
```js
// services/response.js
getServiceSlots: (doctor_uuid, date, serviceItemUuids) => {
const q = serviceItemUuids.map(u => `service_item_uuids[]=${encodeURIComponent(u)}`).join('&');
return api.get(`api/v1/appointment-service-slots?doctor_uuid=${doctor_uuid}&date=${date}&${q}`);
},
```
و اجازهٔ `service_item_uuids` در `postAppointment` (فقط pass-through payload).
منبع لیست سرویس‌های پزشک: `getDoctorServices` (L76, `api/v1/categorys/doctor_services`) یا endpoint سرویس‌های واقعیِ کلینیک اگر پزشک ServiceItem دارد — بررسی کن کدام سرویس‌ها `duration_minutes` دارند (فقط همان‌ها قابل‌انتخاب برای این جریان‌اند).
### ۲. تشخیص حالت پزشک
از پاسخ doctor یا از `getMonthAvailability` مقدار `booking_mode` را بخوان (backend آن را در متای weekly-schedule دارد؛ اگر در پاسخ doctor نبود، از یک فیلد مناسب که backend اضافه می‌کند). در `AppointmentPage`:
- `booking_mode === 'service'` → مرحلهٔ انتخاب سرویس فعال شود.
- در غیر این صورت → **دقیقاً** جریان فعلی (هیچ تغییری).
### ۳. مرحلهٔ انتخاب سرویس (فقط حالت سرویس)
- کامپوننت جدید `components/appointment/service/index.js`: لیست سرویس‌های پزشک با مدت و قیمت؛ انتخاب یک/چند سرویس؛ نمایش «مدت کل» = Σ `duration_minutes`.
- در `Container.js` این مرحله را **قبل** از انتخاب روز قرار بده (حالت سرویس) و state سرویس‌ها را در `AppointmentPage` نگه‌دار.
### ۴. نمایش فقط زمان‌های کافی
- در `app/component/date/dateTime/index.js`: اگر حالت سرویس است، به‌جای `getAppointmentSlots` از `getServiceSlots(uuid, dateStr, selectedServiceUuids)` استفاده کن.
- `lib/appointmentSlots.js`: یک adapter برای پاسخ `data.start_times` (آرایهٔ `{start,end,start_time,location_id}`) اضافه کن که همان ساختار مورد انتظارِ لیست ساعت را بسازد (هر مورد یک دکمهٔ زمان). چون همه از قبل «کافی» هستند، `is_available=true`.
- اگر `start_times` خالی بود: پیام «برای این سرویس در این روز زمان خالی کافی نیست» + هدایت به روز بعدِ دارای ظرفیت (از `getMonthAvailability` برای فعال/غیرفعال بودن روزها استفاده کن).
### ۵. قفل زمان هنگام ثبت
- در `SubmitData.js` به `appointmentPayload` کلید `service_item_uuids: [...]` اضافه کن (حالت سرویس). `slot_start` از انتخاب می‌آید؛ `slot_end` را backend از مدت سرویس محاسبه می‌کند (به مقدار کلاینت اعتماد نمی‌شود) ولی همان `selectedSlot.end` را هم بفرست تا سازگاری حفظ شود.
- منطق قفل دو-مرحله‌ای موجود (`postAppointment``expires_at` → شمارش معکوس در `paying/index.js` → redirect به `payment/order`) دست‌نخورده بماند؛ فقط payload سرویس اضافه می‌شود. مدیریت خطای 409 (reset به step 0، `SubmitData.js:178`) همان بماند.
## نکات مهم
- **«نوبت آزاد» آنلاین نیست.** هیچ مسیری برای `is_reserve` در سایت اضافه نکن؛ فقط منشی در پنل. (در سایت «رزرو» فقط برچسب بازاریابی/عنوان است، نه مدل داده — `Container.js:102`, `payment/[uuid]/page.js:206`.)
- **حالت اسلاتی دست‌نخورده:** وقتی `booking_mode !== 'service'`، هیچ کامپوننت/فراخوانیِ فعلی نباید تغییر رفتار بدهد. مرحلهٔ سرویس فقط شرطی render شود.
- **فقط سرویس‌های دارای مدت:** سرویسی که `duration_minutes` ندارد نباید در این جریان قابل‌انتخاب باشد (backend هم ۴۲۲ می‌دهد) — در UI غیرفعال/مخفی کن.
- **الگوهای پروژه:** App Router، `await params`؛ فراخوانی API از `services/response.js` (interceptor پاسخ را در `services/api.js:73` به `response.data` تبدیل می‌کند)؛ تاریخ Jalali با `moment`/`jalali-moment`؛ RTL، فونت Vazir، MUI v5 + Tailwind؛ رشته‌های UI فارسی. slug پزشک = `uuid`.
- **تست:** به `nobat724-test-suite` پروژه اضافه کن — رندر مرحلهٔ سرویس در حالت سرویس، عدم‌رندر در حالت اسلاتی، محاسبهٔ مدت کل، adapter `start_times`، افزودن `service_item_uuids` به payload.