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:
@@ -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 با پیام توصیفی.
|
||||||
@@ -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) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user