diff --git a/.claude/prompt/appointment-calendar-disabled-dates.md b/.claude/prompt/appointment-calendar-disabled-dates.md new file mode 100644 index 0000000..4b6a8b3 --- /dev/null +++ b/.claude/prompt/appointment-calendar-disabled-dates.md @@ -0,0 +1,132 @@ +# نمایش روزهای تعطیل/غیرقابل‌انتخاب روی تقویم صفحه‌ی نوبت + احترام به بازه‌ی رزرو + +## پروژه + +`nobat724_front` — سایت عمومی نوبت‌دهی. **این پرامپت بعد از پرامپت backend اجرا شود.** + +> **Cross-repo:** این کار به endpoint جدید backend وابسته است: +> `GET /api/v1/appointment-settings/month-availability/{doctorUuid}?year=&month=` +> که در پرامپت `clinicpro/.claude/prompt/doctor-booking-window-and-month-availability.md` ساخته می‌شود. اگر آن endpoint هنوز نیست، **متوقف شو و اول backend را اجرا کن.** + +## زمینه + +تقویم دوماهه‌ی صفحه‌ی نوبت (`/appointment/[doctorId]`) الان همه‌ی روزهای آینده را قابل‌انتخاب نشان می‌دهد، چون در اصلاح قبلی فراخوانی endpoint ناموجود `appointment/not-available` حذف شد و `disabledDates={[]}` پاس داده می‌شود. در نتیجه روزهای تعطیلِ پزشک (مثلاً ۲۷/۰۳/۱۴۰۵ که با date override بسته شده) روی تقویم خاکستری نمی‌شوند و کاربر می‌تواند رویشان کلیک کند — بعد فقط لیست اسلات خالی می‌بیند. همچنین بازه‌ی رزرو (مثلاً «تا ۲ ماه جلوتر») روی تقویم اعمال نمی‌شود. + +## مشکل / هدف + +تقویم باید روزهای غیرقابل‌انتخاب را خاکستری و disable کند. این روزها از endpoint `month-availability` می‌آیند: تعطیلات، date overrideهای بسته، روزهای بدون شیفت، و روزهای خارج از بازه‌ی رزرو پزشک. چون تقویم دوماهه است، باید برای **هر دو ماه نمایش‌داده‌شده** داده گرفته شود و با تغییر ماه (navigation) داده‌ی ماه جدید هم لود شود. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `services/response.js` | افزودن `getMonthAvailability(doctorUuid, year, month)` | +| `app/appointment/[doctorId]/page.js` | الان `disabledDates={[]}` می‌فرستد (سرور-ساید) | +| `components/appointment/index.js` → `Container` → `date/index.js` → `Time` → `SelectDatePicker` | زنجیره‌ی پاس‌دادن `disabledDates` | +| `app/component/date/datePicker/index.js` | تقویم دوماهه‌ی client؛ `disabledDates` آرایه‌ی Unix timestamp است | +| `components/common/InlineJalaliMonth.js` | رندر یک ماه؛ `isDisabled(date)` روز را خاکستری/disable می‌کند | +| `lib/appointmentSlots.js` | adapter موجود اسلات‌ها (الگوی adapter جدا) | + +## وضعیت فعلی (کد واقعی) + +### `app/appointment/[doctorId]/page.js` — disabledDates خالی + +```js +let doctor = null; +try { + const doctorRes = await axiosInstance.get(`${API_URL}/api/v1/doctor/${doctorId}`); + doctor = doctorRes.data?.data?.data; +} catch (error) {} + +return ( + +); +``` + +### `app/component/date/datePicker/index.js` — مصرف disabledDates + +```js +function DatePicker({ setDate, disabledDates = [] }) { + const [baseMonth, setBaseMonth] = useState(moment().tz("Asia/Tehran")); + // ... + const isDisabled = (date) => { + const day = moment(date).startOf("day"); + if (day.isBefore(moment().startOf("day"))) return true; + return disabledDates.some((ts) => day.isSame(moment.unix(ts).startOf("day"))); + }; + // auto-select nearest available day از همین isDisabled استفاده می‌کند + // دو InlineJalaliMonth: baseMonth (راست) + secondMonth = baseMonth+1 (چپ) +} +``` + +> `disabledDates` آرایه‌ی Unix timestamp (ثانیه) است؛ `isDisabled` گذشته را هم می‌بندد. `baseMonth` با فلش‌های ناوبری تغییر می‌کند. + +### `services/response.js` — توابع slot موجود + +```js +getAppointmentSlots: (doctor_uuid, date) => + api.get(`api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}`, removeTokenHead), +``` + +## قرارداد endpoint جدید backend (مرجع) + +``` +GET /api/v1/appointment-settings/month-availability/{doctorUuid}?year=&month= (PUBLIC) +``` + +```json +{ + "success": true, + "data": { + "year": 2026, + "month": 6, + "disabled_dates": ["2026-06-16", "2026-06-20"], + "online_booking_enabled": true, + "booking_window": { "value": 2, "unit": "month" } + } +} +``` + +> `disabled_dates` فرمت `Y-m-d` میلادی. **قبل از پیاده‌سازی، با پرامپت backend چک کن که endpoint ورودی شمسی می‌خواهد یا میلادی** — اگر میلادی است، ماه شمسیِ نمایش‌داده‌شده را باید به بازه‌ی میلادی تبدیل و برای ماه(های) میلادی متناظر صدا بزنی. interceptor `services/api.js` یک‌بار پاسخ را باز می‌کند → داده در `res.data`. + +## وظایف + +اجرای مرحله‌به‌مرحله؛ بعد از هر قابلیت `npm run build` و سپس commit جدا. + +### ۱. افزودن `getMonthAvailability` به `services/response.js` + +```js +getMonthAvailability: (doctor_uuid, year, month) => + api.get(`api/v1/appointment-settings/month-availability/${doctor_uuid}`, { + params: { year, month }, + }), +``` + +(public — بدون `requireAuth`؛ از همان الگوی `getAppointmentSlots` پیروی کن.) + +### ۲. لود داده‌ی در‌دسترس‌بودن در `DatePicker` و disable‌کردن روزها + +- چون `DatePicker` کامپوننت client است و `doctor.uuid` در آن در دسترس نیست (الان فقط `setDate`/`disabledDates` می‌گیرد)، یا `doctorUuid` را به‌عنوان prop از زنجیره پاس بده، یا از `useParams().doctorId` داخل `DatePicker` بخوان (همان uuid است — الگوی موجود `SendAppo`). +- داخل `DatePicker` یک state `disabledTimestamps` نگه‌دار و با `useEffect` وابسته به `baseMonth`: + - برای ماهِ `baseMonth` و ماهِ `secondMonth` (baseMonth+1) `getMonthAvailability` را صدا بزن (تبدیل ماه نمایشی به year/month مطابق قرارداد backend). + - `disabled_dates` (`Y-m-d`) را به Unix timestamp (`moment(d,"YYYY-MM-DD").startOf("day").unix()`) تبدیل و در state بریز. +- `isDisabled` فعلی را نگه‌دار ولی منبع `disabledDates` را از این state بگیر (یا prop `disabledDates` را با state داخلی merge کن). گذشته هم‌چنان بسته بماند. +- **auto-select**: منطق انتخاب نزدیک‌ترین روز قابل‌انتخاب باید بعد از لود `disabledTimestamps` اجرا/بازاجرا شود تا روی روز تعطیل auto-select نشود. + +### ۳. هندل ناوبری ماه + +- با کلیک فلش‌ها `baseMonth` تغییر می‌کند → `useEffect` دوباره داده‌ی دو ماه جدید را می‌گیرد. مطمئن شو state تجمعی است (ماه‌های قبلی پاک نشوند یا حداقل ماه‌های نمایش‌فعلی پوشش داده شوند) و درخواست تکراری بی‌مورد نزن (می‌توانی ماه‌های لودشده را cache کنی با یک `Set`/object key=`year-month`). + +### ۴. (اختیاری ولی توصیه‌شده) نمایش وضعیت نوبت‌دهی خاموش + +- اگر `online_booking_enabled === false` در پاسخ، به‌جای تقویم یک پیام «نوبت‌دهی آنلاین این پزشک غیرفعال است» نشان بده و دکمه‌ی «تایید نوبت» را disable کن. (اگر می‌خواهی این بخش را جدا کنی، در گزارش ذکر کن.) + +## نکات مهم + +- **وابستگی cross-repo:** بدون endpoint `month-availability` این کار کامل نمی‌شود. اگر نبود متوقف شو. +- **شمسی/میلادی:** تقویم سایت شمسی است؛ endpoint احتمالاً میلادی می‌خواهد. تبدیل را با `moment-jalaali` انجام بده و **حدس نزن** — قرارداد دقیق را از پرامپت/داک backend بگیر. یک ماه شمسی روی دو ماه میلادی می‌افتد؛ یا برای پوشش کامل، بازه‌ی میلادیِ روزهای نمایش‌داده‌شده را محاسبه کن. +- **timestampها Unix (ثانیه):** `disabledDates` که `isDisabled` می‌خواند آرایه‌ی Unix ثانیه است؛ تبدیل `Y-m-d → unix` را با `startOf("day")` در `Asia/Tehran` انجام بده تا با منطق فعلی هم‌خوان شود. +- **interceptor:** `request.*` بدنه را یک‌بار باز می‌کند → `res.data.disabled_dates`. +- **عدم رگرسیون:** تقویم دوماهه، ترتیب RTL (خرداد راست، تیر چپ)، فلش‌ها، و انتخاب خودکار نباید بشکنند. `InlineJalaliMonth` فقط `isDisabled` را مصرف می‌کند — منطق رنگ خاکستری از قبل هست. +- **Multi-domain/Jalali/RTL:** حفظ شوند. +- **تست:** `npm run build`؛ سپس دستی با پزشک `4a0594b1-008b-478a-a593-259b95d8c2dd` که یک date override بسته روی ۲۷/۰۳/۱۴۰۵ دارد — آن روز باید خاکستری و غیرقابل‌کلیک باشد، و روزهای خارج از بازه‌ی رزرو هم همین‌طور. سپس commit با پیام توصیفی. diff --git a/services/response.js b/services/response.js index eb270e2..fb5af0f 100644 --- a/services/response.js +++ b/services/response.js @@ -64,6 +64,11 @@ export const request = { `api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}`, removeTokenHead ), + getMonthAvailability: (doctor_uuid, year, month) => + api.get(`api/v1/appointment-settings/month-availability/${doctor_uuid}`, { + params: { year, month }, + ...removeTokenHead, + }), postAppointment: (data) => api.post(`api/v1/appointment`, data, { requireAuth: true }), getMyAppointments: (userId, params) => api.get(`api/v1/appointment/my-appointments/${userId}`, { params, requireAuth: true }), postAppointmentPayment: (data) =>