# نمایش محل‌های نوبت‌دهی فقط بر اساس برنامهٔ همان روز ## پروژه `nobat724_front` پرامپت همتای backend که **باید اول اجرا شود**: `clinicpro/.claude/prompt/fix-booking-context-slots-and-phantom-locations.md` ## زمینه صفحهٔ رزرو حالا محل نوبت‌دهی (مطب شخصی / کلینیک) را از `GET /api/v1/appointment-booking-locations/{doctorUuid}` می‌گیرد و کاربر یکی را انتخاب می‌کند. اما این فهرست **وضعیت واقعی رزرو در یک روز مشخص** را نشان نمی‌دهد. برای «دکتر تست» (`bcabb3a8-cae3-45ec-876c-548f9c1e1569`) گزینهٔ «مطب شخصی» نمایش داده می‌شود، در حالی که در دیتابیس این پزشک **هیچ آدرس شخصی ثبت‌شده‌ای ندارد** و شیفت برنامهٔ شخصی‌اش هم `location_id = NULL` است. یعنی محلی که اصلاً قابل رزرو نیست، به بیمار پیشنهاد می‌شود. ## مشکل / هدف قاعدهٔ درست نمایش یک محل: 1. برای آن محل حداقل یک **آدرس ثبت‌شده** وجود داشته باشد، **و** 2. در برنامهٔ کاری، همان آدرس برای شیفت‌های آن محل **انتخاب شده** باشد، **و** 3. برای **روز انتخاب‌شده** حداقل یک اسلات آزاد داشته باشد. اگر هر کدام برقرار نباشد، آن محل نباید به‌عنوان گزینهٔ قابل انتخاب نمایش داده شود — نه در `/appointment/[doctorId]` و نه در پروفایل پزشک `/doctor/[slug]`. شرط‌های ۱ و ۲ در backend اعمال می‌شوند (پرامپت همتا). این پرامپت شرط ۳ و مصرف درست فهرست را پوشش می‌دهد. ## قرارداد API بعد از تغییر backend ``` GET /api/v1/appointment-booking-locations/{doctorUuid} GET /api/v1/appointment-booking-locations/{doctorUuid}?date=YYYY-MM-DD ``` - بدون `date`: فقط محل‌های **معتبر** (آدرس دارند و شیفت روی آدرس دارند). محل بدون آدرس دیگر اصلاً برنمی‌گردد. - با `date`: هر آیتم فیلد `available_on_date` (بولین) می‌گیرد و پاسخ `date` را echo می‌کند. - `opening_hours` هر آیتم حالا `day_index` و `location_id` هم دارد. بقیهٔ قرارداد بدون تغییر: مرتب بر اساس `next_available_at` صعودی، پاسخ double-nested (`json.data.data`). ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `services/response.js` | `getBookingLocations` — باید `date` بگیرد | | `components/appointment/index.js` | `AppointmentPage` — نگه‌دارندهٔ `bookingLocations` و `selectedLocation` | | `components/appointment/Container.js` | مرحلهٔ انتخاب محل / سرویس / تاریخ | | `components/appointment/location/LocationSelect.js` | کارت‌های انتخاب محل | | `components/appointment/date/index.js` | مرحلهٔ تاریخ + دکمهٔ «تغییر محل» | | `app/component/date/datePicker/index.js` | تقویم ماه (`getMonthAvailability`) | | `app/doctor/[slug]/page.js` | پروفایل پزشک + JSON-LD | | `components/doctor/detailDoctor/cards/locations/index.js` | کارت «موقعیت مکانی» در پروفایل | ## وضعیت فعلی ### فهرست محل‌ها وابسته به روز نیست `components/appointment/index.js` — یک‌بار در mount گرفته می‌شود و تا آخر ثابت می‌ماند: ```js useEffect(() => { if (!doctor?.uuid) return; request .getBookingLocations(doctor.uuid) .then((res) => { const d = res?.data?.data ?? res?.data ?? {}; const list = Array.isArray(d.booking_locations) ? d.booking_locations : []; setBookingLocations(list); ... }) ``` `selectedDate` بعداً در همین کامپوننت ست می‌شود ولی هیچ‌وقت به این فراخوانی برنمی‌گردد. ### پروفایل پزشک همهٔ آدرس‌ها را نشان می‌دهد `app/doctor/[slug]/page.js` — `workLocation` در JSON-LD و کارت «موقعیت مکانی» از `getDoctorAddresses(doctor.id)` می‌آیند که **همهٔ** آدرس‌ها را برمی‌گرداند، مستقل از اینکه در برنامهٔ نوبت‌دهی استفاده شده باشند یا نه. ## وظایف ### ۱. `date` در لایهٔ سرویس `services/response.js`: ```js getBookingLocations: (doctor_uuid, date = null) => api.get(`api/v1/appointment-booking-locations/${doctor_uuid}`, { params: date ? { date } : {}, ...removeTokenHead, }), ``` ### ۲. واکشی دوباره با تغییر روز در `components/appointment/index.js`: - فهرست اولیه بدون `date` گرفته شود (برای مرحلهٔ انتخاب محل، قبل از انتخاب روز). - بعد از انتخاب روز، دوباره با `date` گرفته شود و `available_on_date` روی کارت‌ها اعمال شود. `selectedDate` در این کامپوننت **timestamp** است؛ برای پارامتر API باید به `YYYY-MM-DD` تبدیل شود (از `moment-jalaali` که در پروژه هست استفاده کن، همان الگوی `app/component/date/dateTime/index.js` که `moment.unix(date).format("YYYY-MM-DD")` می‌زند). **حالت مرزی مهم:** اگر روزِ انتخاب‌شده محلِ انتخاب‌شده را غیرفعال کند (`available_on_date === false`)، نباید بی‌صدا اسلات خالی نشان دهی. یا کاربر را به مرحلهٔ انتخاب محل برگردان با پیام روشن، یا خودکار به اولین محلِ باز در آن روز سوییچ کن و این جابه‌جایی را اطلاع بده. **بی‌صدا نگه‌داشتن محلِ بسته = صفحهٔ خالی بدون توضیح.** ### ۳. UI کارت‌های محل `components/appointment/location/LocationSelect.js`: - وقتی `available_on_date === false`، کارت غیرفعال شود (`disabled`، `cursor-not-allowed`، کم‌رنگ) با برچسب «در این روز نوبت ندارد». - کارت غیرفعال قابل کلیک نباشد. - اگر **هیچ** محلی در آن روز باز نبود، پیام روشن بده و کاربر را به انتخاب روز دیگر هدایت کن. از تم و کامپوننت‌های موجود استفاده کن (MUI v5 + Tailwind، RTL، فونت Vazir) — طراحی جدید نساز. ### ۴. تقویم ماه هم per-location است `app/component/date/datePicker/index.js` الان `clinicUuid` می‌گیرد و `getMonthAvailability` را با آن صدا می‌زند — این درست است و نیازی به تغییر ندارد. فقط مطمئن شو بعد از سوییچ محل، کش ماه‌ها پاک می‌شود (همان effect موجود روی `clinicUuid`). ### ۵. پروفایل پزشک — فقط محل‌های قابل رزرو در `app/doctor/[slug]/page.js`: - `getBookingLocations` (بدون `date`) را server-side بگیر — همین الان برای `availableService` و `openingHoursSpecification` گرفته می‌شود. - کارت «موقعیت مکانی» (`components/doctor/detailDoctor/cards/locations/`) و `workLocation` در JSON-LD باید **فقط** آدرس‌هایی را نشان دهند که `location_uuid` آن‌ها در `booking_locations` آمده است. ```js const bookableUuids = new Set( bookingLocations.map((l) => l.location_uuid).filter(Boolean) ); const bookableAddresses = addresses.filter((a) => bookableUuids.has(a.uuid)); ``` - **تصمیم لازم:** آدرسی که ثبت شده ولی در هیچ برنامه‌ای استفاده نشده، اطلاعات واقعی مطب است و حذف کاملش از پروفایل ممکن است خواسته نباشد. پیشنهاد: در کارت «موقعیت مکانی» نمایش داده شود ولی بدون دکمهٔ «دریافت نوبت»، و در JSON-LD **نیاید** (چون schema.org آن را قابل مراجعه اعلام می‌کند). اگر تصمیم دیگری گرفتی، در PR بنویس. ### ۶. لینک CTA `components/doctor/appointmentList/index.js` — اگر پروفایل محل مشخصی را برجسته کرد، CTA همان را حمل کند: `/appointment/${doctorSlug}?clinic_uuid=${clinicUuid}`. اگر محلی برجسته نشده، پارامتر ندهد تا صفحهٔ رزرو خودش انتخابگر را نشان دهد (رفتار فعلی، درست است). ## نکات مهم - **backend اول.** تا وقتی فیلتر سمت سرور اعمال نشده، «مطب شخصی» جعلی همچنان برمی‌گردد و کار فرانت قابل تست نیست. - پاسخ double-nested: `json?.data?.data ?? json?.data`. - endpoint عمومی است و `removeTokenHead` می‌گیرد؛ برای رزرو نهایی `{ requireAuth: true }`. - تاریخ‌ها شمسی با `jalali-moment` / `moment-jalaali`؛ رشته‌های جدید فارسی؛ RTL. - slug پزشک = `uuid`. - هر صفحه `generateMetadata` صادر کند و `params` همیشه `await` شود. - تست دستی: «دکتر تست» (`bcabb3a8-cae3-45ec-876c-548f9c1e1569`) — بعد از فیلتر backend نباید هیچ گزینهٔ «مطب شخصی» ببیند، فقط کلینیک «علی بهروزی». روزی که کلینیک شیفت ندارد هم باید محل را غیرفعال نشان دهد. - بعد از تغییرات: `npm run lint` و `npm run build` هر دو سبز.