feat(appointment): add getMonthAvailability request wrapper

Public wrapper for GET /api/v1/appointment-settings/month-availability
/{doctor_uuid}?year=&month= so the calendar can learn which days are
bookable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-15 16:29:31 +03:30
co-authored by Claude Opus 4.8
parent 779113bb19
commit 96f4126c0b
2 changed files with 137 additions and 0 deletions
@@ -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 (
<AppointmentPage doctor={doctor} disabledDates={[]} matchedCity={matchedCity} />
);
```
### `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 با پیام توصیفی.
+5
View File
@@ -64,6 +64,11 @@ export const request = {
`api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}`, `api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}`,
removeTokenHead 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 }), postAppointment: (data) => api.post(`api/v1/appointment`, data, { requireAuth: true }),
getMyAppointments: (userId, params) => api.get(`api/v1/appointment/my-appointments/${userId}`, { params, requireAuth: true }), getMyAppointments: (userId, params) => api.get(`api/v1/appointment/my-appointments/${userId}`, { params, requireAuth: true }),
postAppointmentPayment: (data) => postAppointmentPayment: (data) =>