108 lines
8.7 KiB
Markdown
108 lines
8.7 KiB
Markdown
# نوبتدهی آنلاین بر اساس سرویس (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.
|