The site offered a "personal practice" for a doctor who has no personal address at all — the schedule existed but its shifts pointed at the clinic's address, so there was nowhere to go. The backend now filters those out; this consumes the filtered contract and adds the per-day dimension. - getBookingLocations takes an optional date and the appointment page refetches on it, merging available_on_date into the existing list rather than replacing it, so browsing the calendar never resets the user's choice. - The browsed day had to be lifted out of the Date step: selectedDate is only set once a slot is confirmed, far too late to drive availability. - A location closed on the chosen day renders disabled with «در این روز نوبت ندارد», and when every location is closed the step says so instead of showing an empty slot list. If the already-selected location closes, a notice appears with a link back to the picker — silently showing nothing was the failure mode worth avoiding. - Doctor profile: workLocation in the Physician JSON-LD is limited to addresses that appear in booking_locations, since schema.org presents them as places a patient can attend. The address card still lists the others — they are real practice details — tagged «بدون نوبتدهی آنلاین». Verified end-to-end with a temporary unused address on the test doctor: the visible card listed both and tagged the unused one, while workLocation carried only the bookable one. The row was removed afterwards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.8 KiB
نمایش محلهای نوبتدهی فقط بر اساس برنامهٔ همان روز
پروژه
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 است. یعنی محلی که اصلاً قابل رزرو نیست، به بیمار پیشنهاد میشود.
مشکل / هدف
قاعدهٔ درست نمایش یک محل:
- برای آن محل حداقل یک آدرس ثبتشده وجود داشته باشد، و
- در برنامهٔ کاری، همان آدرس برای شیفتهای آن محل انتخاب شده باشد، و
- برای روز انتخابشده حداقل یک اسلات آزاد داشته باشد.
اگر هر کدام برقرار نباشد، آن محل نباید بهعنوان گزینهٔ قابل انتخاب نمایش داده شود — نه در
/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 گرفته میشود و تا آخر ثابت میماند:
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:
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آمده است.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هر دو سبز.