diff --git a/.claude/prompt/doctor-profile-booking-state-from-locations.md b/.claude/prompt/doctor-profile-booking-state-from-locations.md new file mode 100644 index 0000000..1fa4fc1 --- /dev/null +++ b/.claude/prompt/doctor-profile-booking-state-from-locations.md @@ -0,0 +1,131 @@ +# وضعیت «نوبت‌دهی فعال/غیرفعال» پروفایل پزشک از booking_locations + +## پروژه + +`nobat724_front` + +پرامپت همتای backend که **باید اول اجرا شود**: +`clinicpro/.claude/prompt/fix-public-booking-state-and-admin-context-slots.md` + +## زمینه + +برای «دکتر تست» (`bcabb3a8-cae3-45ec-876c-548f9c1e1569`) صفحهٔ +`/doctor/bcabb3a8-…` پیام «نوبت‌دهی غیرفعال است» نشان می‌دهد در حالی که نوبت‌دهی +کلینیکش فعال است و همین صفحه، `booking_locations` معتبر (با `next_available_at` غیرتهی) +را server-side می‌گیرد. + +علت (با curl تأیید شد): فیلدهای `active` / `free_turn` پاسخ `GET /api/v1/doctor/{uuid}` +در backend فقط از **برنامهٔ شخصی** پزشک ساخته می‌شدند؛ برنامهٔ شخصیِ دکتر تست غیرفعال +است و برنامهٔ کلینیکش دیده نمی‌شد. پرامپت همتا این را با تجمیع همهٔ برنامه‌ها اصلاح می‌کند. + +این پرامپت سمت سایت را defensive می‌کند: بنر و دکمهٔ نوبت‌دهی پروفایل نباید فقط به +`doctor.active` تکیه کند وقتی خودِ صفحه دادهٔ دقیق‌تر (`booking_locations`) را در دست دارد. +دو منبع نباید بتوانند حرف متناقض بزنند. + +## مشکل / هدف + +قاعدهٔ واحد برای پروفایل پزشک: + +- **نوبت‌دهی فعال** ⇔ `booking_locations` غیرخالی است (backend فقط محل‌های واقعاً + قابل‌رزرو را برمی‌گرداند: آدرس‌دار + شیفت فعال روی همان آدرس). +- برچسب «اولین نوبت آزاد» از کمینهٔ `next_available_at` بین محل‌ها؛ اگر همه `null` بودند، + محل‌ها فعالند ولی فعلاً ظرفیت ندارند → «فعلاً نوبت خالی ندارد» (نه «غیرفعال»). +- `doctor.active === false` همراه با `booking_locations` خالی → «نوبت‌دهی غیرفعال است» + (رفتار فعلی، درست). + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `app/doctor/[slug]/page.js:43,128-150` | `getBookingLocations` server-side — از قبل موجود | +| `components/doctor/appointmentList/index.js:8,20-24` | نوار موبایل — `doctor?.active === false \|\| !doctor?.free_turn` | +| `components/doctor/appointmentList/ItemAppointment.js:10,42-56` | کارت دسکتاپ — `bookingDisabled = turn?.active === false` | +| `app/component/ItemDoctor.js:89` | کارت لیست پزشکان — فقط فیلد backend (بدون تغییر) | + +## وضعیت فعلی + +### کارت دسکتاپ — `components/doctor/appointmentList/ItemAppointment.js:10` + +```js +function ItemAppointment({ turn, loading, doctorSlug }) { + const bookingDisabled = !loading && turn?.active === false; + ... + {bookingDisabled || !turn?.free_turn + ? "نوبت‌دهی غیرفعال است" + : `اولین نوبت آزاد: ${turn.free_turn}`} +``` + +### نوار موبایل — `components/doctor/appointmentList/index.js:20-24` + +```js + {doctor?.active === false || !doctor?.free_turn + ? "نوبت‌دهی غیرفعال است" + : `اولین نوبت آزاد: ${doctor.free_turn}`} +``` + +`AppointmentList` از `app/doctor/[slug]/page.js` رندر می‌شود که `bookingLocations` را +همان‌جا دارد (`:128-129`) ولی به این کامپوننت پاس نمی‌دهد. + +## وظایف + +### ۱. پاس‌دادن `bookingLocations` به AppointmentList + +در `app/doctor/[slug]/page.js` همان آرایهٔ گرفته‌شده را prop بده: + +```jsx + +``` + +(نام prop و محل رندر را با کد واقعی صفحه تطبیق بده — `AppointmentList` ممکن است از طریق +کامپوننت میانی رندر شود؛ در آن صورت prop را از همان مسیر عبور بده.) + +### ۲. قاعدهٔ واحد فعال/غیرفعال + +یک helper کوچک در همان `components/doctor/appointmentList/` (نه util سراسری جدید): + +```js +export function bookingState(doctor, bookingLocations) { + const hasLocations = Array.isArray(bookingLocations) && bookingLocations.length > 0; + if (!hasLocations && doctor?.active === false) return { enabled: false, label: "نوبت‌دهی غیرفعال است" }; + if (!hasLocations) return { enabled: doctor?.active !== false, label: doctor?.free_turn ? `اولین نوبت آزاد: ${doctor.free_turn}` : "نوبت‌دهی غیرفعال است" }; + const earliest = bookingLocations + .map((l) => l.next_available_at) + .filter((t) => t != null) + .sort((a, b) => a - b)[0] ?? null; + return { + enabled: true, + label: earliest != null + ? `اولین نوبت آزاد: ${formatJalali(earliest)}` + : "فعلاً نوبت خالی ندارد", + }; +} +``` + +- `formatJalali` با `jalali-moment`/`moment-jalaali` موجود پروژه: روزِ هفته + ساعت + (مثل «شنبه ۰۹:۰۰») تا با فرمت `free_turn` backend هم‌خانواده باشد. الگوی + `moment.unix(ts)` مثل `app/component/date/dateTime/index.js`. +- هر دو کامپوننت (`index.js` نوار موبایل و `ItemAppointment.js`) از همین helper استفاده + کنند؛ شرط‌های تکراری فعلی حذف شوند. +- دکمهٔ «دریافت نوبت» با `enabled === true` فعال است حتی وقتی `earliest === null` + (صفحهٔ رزرو خودش روزهای بدون ظرفیت را نشان می‌دهد). + +### ۳. کارت لیست پزشکان — بدون تغییر + +`app/component/ItemDoctor.js:89` فقط `free_turn`/`active` پاسخ لیست را دارد و +`booking_locations` برای هر آیتم لیست fetch نمی‌شود (N درخواست اضافه ممنوع). بعد از فیکس +backend همین فیلدها درست می‌شوند — این فایل را دست نزن. + +## نکات مهم + +- **backend اول.** قبل از اجرای پرامپت همتا، `doctor.active` برای دکتر تست همچنان false + برمی‌گردد؛ helper این را می‌پوشاند ولی تست کامل فقط بعد از هر دو ممکن است. +- پاسخ‌های API double-nested: `json?.data?.data ?? json?.data` — `getBookingLocations` + موجود در صفحه همین را رعایت می‌کند؛ عوضش نکن. +- `booking_locations` فقط محل‌های معتبر را دارد (فیلتر backend از پرامپت‌های قبلی) — + دوباره فیلتر نکن. +- رشته‌های جدید فارسی، RTL، تم موجود (MUI v5 + Tailwind، فونت Vazir) — طراحی جدید نساز. +- slug پزشک = `uuid`؛ `params` همیشه `await`؛ صفحه `generateMetadata` دارد — دست نزن. +- تست دستی: `/doctor/bcabb3a8-cae3-45ec-876c-548f9c1e1569` باید «اولین نوبت آزاد» و دکمهٔ + فعال «دریافت نوبت» نشان دهد (دکتر تست فقط برنامهٔ کلینیکی فعال دارد). یک پزشک واقعاً + غیرفعال (بدون هیچ محل) همچنان «نوبت‌دهی غیرفعال است» ببیند. +- بعد از تغییرات: `npm run lint` و `npm run build` هر دو سبز. diff --git a/app/doctor/[slug]/page.js b/app/doctor/[slug]/page.js index 1ad6582..6c8ff71 100644 --- a/app/doctor/[slug]/page.js +++ b/app/doctor/[slug]/page.js @@ -267,6 +267,7 @@ async function Doctor({ params }) { rateAggregate={rateAggregate} addresses={addresses} bookableAddressUuids={[...bookableAddressUuids]} + bookingLocations={bookingLocations} slug={slug} /> diff --git a/components/doctor/appointmentList/ItemAppointment.js b/components/doctor/appointmentList/ItemAppointment.js index 3e69014..852f409 100644 --- a/components/doctor/appointmentList/ItemAppointment.js +++ b/components/doctor/appointmentList/ItemAppointment.js @@ -5,9 +5,11 @@ import GreenCalendarTurn from "@/components/icons/GreenCalendarTurn"; import { Button } from "@mui/material"; import Link from "next/link"; import DoctorAvatar from "@/app/component/DoctorAvatar"; +import { bookingState } from "./bookingState"; -function ItemAppointment({ turn, loading, doctorSlug }) { - const bookingDisabled = !loading && turn?.active === false; +function ItemAppointment({ turn, loading, doctorSlug, booking }) { + const state = booking ?? bookingState(turn, []); + const bookingDisabled = !loading && !state.enabled; return (
  • - {bookingDisabled || !turn?.free_turn - ? "نوبت‌دهی غیرفعال است" - : `اولین نوبت آزاد: ${turn.free_turn}`} + {state.label}

    diff --git a/components/doctor/appointmentList/bookingState.js b/components/doctor/appointmentList/bookingState.js new file mode 100644 index 0000000..5882dbd --- /dev/null +++ b/components/doctor/appointmentList/bookingState.js @@ -0,0 +1,53 @@ +import moment from "moment-jalaali"; + +const DAY_NAMES = [ + "یکشنبه", + "دوشنبه", + "سه‌شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه", +]; + +const formatJalali = (ts) => { + const m = moment.unix(ts); + return `${DAY_NAMES[m.day()]} ${m.format("HH:mm")}`; +}; + +/** + * قاعدهٔ واحد فعال/غیرفعال بودن نوبت‌دهی در پروفایل پزشک. + * + * booking_locations دقیق‌تر از doctor.active است: backend فقط محل‌های واقعاً + * قابل‌رزرو را برمی‌گرداند. تا وقتی حداقل یک محل هست، نوبت‌دهی فعال است — حتی + * اگر فیلد قدیمی active به هر دلیل false باشد. بدون محل، همان فیلدهای doctor + * ملاک می‌مانند (سازگاری با پزشک بدون برنامه). + */ +export function bookingState(doctor, bookingLocations) { + const hasLocations = + Array.isArray(bookingLocations) && bookingLocations.length > 0; + + if (!hasLocations) { + const enabled = doctor?.active !== false && !!doctor?.free_turn; + return { + enabled, + label: enabled + ? `اولین نوبت آزاد: ${doctor.free_turn}` + : "نوبت‌دهی غیرفعال است", + }; + } + + const earliest = + bookingLocations + .map((l) => l.next_available_at) + .filter((t) => t != null) + .sort((a, b) => a - b)[0] ?? null; + + return { + enabled: true, + label: + earliest != null + ? `اولین نوبت آزاد: ${formatJalali(earliest)}` + : "فعلاً نوبت خالی ندارد", + }; +} diff --git a/components/doctor/appointmentList/index.js b/components/doctor/appointmentList/index.js index 0a0a818..0d85b1b 100644 --- a/components/doctor/appointmentList/index.js +++ b/components/doctor/appointmentList/index.js @@ -4,24 +4,30 @@ import { Button } from "@mui/material"; import ItemAppointment from "./ItemAppointment"; import ArrowLeftD from "@/components/icons/ArrowLeftD"; import Link from "next/link"; +import { bookingState } from "./bookingState"; -function AppointmentList({ doctor, loading, doctorSlug }) { +function AppointmentList({ doctor, loading, doctorSlug, bookingLocations = [] }) { + const booking = bookingState(doctor, bookingLocations); return ( <>

    - {doctor?.active === false || !doctor?.free_turn - ? "نوبت‌دهی غیرفعال است" - : `اولین نوبت آزاد: ${doctor.free_turn}`} + {booking.label}

    - {doctor?.active === false ? ( + {!booking.enabled ? (