Files
clinicpro/.claude/prompt/doctor-booking-window-and-month-availability.md
T
hamedandClaude Opus 4.8 09aba878f8 feat(appointment): store online-booking window in WeeklySchedule meta
Add a reserved meta key inside WeeklySchedule.setting (no migration) holding
online_booking_enabled and a booking_window value+unit (week|month), with
sane defaults. Expose meta separately in toArray() and keep it out of the
day schedule map. setSetting now preserves meta when the day schedule is
replaced; the weekly-schedule POST/PATCH endpoints accept an optional meta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:16:30 +03:30

177 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# تنظیمات نوبت‌دهی آنلاین دکتر: بازه‌ی رزرو (booking window) + endpoint در‌دسترس‌بودن ماهانه
## پروژه
`clinicpro` (Backend + Admin React panel). **این پرامپت اول اجرا شود.**
> **Cross-repo:** قرارداد API این پرامپت توسط سایت عمومی مصرف می‌شود. پرامپت همتا در سمت فرانت:
> `nobat724_front/.claude/prompt/appointment-calendar-disabled-dates.md`
## زمینه
پزشک در پنل ادمین (`assets/admin/pages/DoctorDetailPage.tsx`) از قبل می‌تواند **برنامه‌ی هفتگی**، **date override** (روز تعطیل/سفارشی) و **تعطیلات (holidays)** را تنظیم کند. `SlotCalculatorService` هم این‌ها را با اولویت درست اعمال می‌کند (holiday → override → weekly). پس «۲۷/۰۳/۱۴۰۵ تعطیل است» همین حالا با یک date override (`active:false`) قابل تنظیم است و `appointment-slots` برای آن روز آرایه‌ی خالی برمی‌گرداند.
اما **دو چیز وجود ندارد:**
1. **بازه‌ی رزرو آنلاین (booking window):** پزشک نمی‌تواند تعیین کند «تا چند هفته/ماه جلوتر بیمار می‌تواند آنلاین نوبت بگیرد» و نمی‌تواند نوبت‌دهی آنلاین را کلاً خاموش کند. الان هیچ سقفی نیست و `appointment-slots` برای هر تاریخ آینده‌ای اسلات می‌دهد.
2. **در‌دسترس‌بودن ماهانه (month availability):** سایت عمومی برای خاکستری‌کردن روزهای تعطیل/خارج‌از‌بازه روی تقویم، باید بداند کدام روزهای یک ماه قابل‌انتخاب‌اند. الان فقط endpoint تک‌روزه (`appointment-slots`) هست؛ صدا‌زدن آن برای ۳۰ روز سنگین است. یک endpoint که برای یک ماه، آرایه‌ی روزهای غیرفعال را برگرداند لازم است.
## مشکل / هدف
۱. افزودن تنظیمات «نوبت‌دهی آنلاین» به پزشک: `online_booking_enabled` (boolean) و `booking_window` (عدد + واحد هفته/ماه). بدون migration — داخل همان `WeeklySchedule.setting` JSON ذخیره شود.
۲. اعمال این بازه در `SlotCalculatorService`: اگر نوبت‌دهی آنلاین خاموش است یا تاریخ خارج از بازه است → اسلات خالی.
۳. endpoint عمومی جدید: `GET /api/v1/appointment-settings/month-availability/{doctorUuid}?year=&month=` که برای یک ماه شمسی **یا** میلادی، روزهای غیرفعال (تعطیل/override بسته/خارج‌از‌بازه/بدون session) را برگرداند تا تقویم سایت آن‌ها را غیرقابل‌انتخاب کند.
۴. UI در پنل ادمین (`DoctorDetailPage.tsx`) برای تنظیم این دو مقدار.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Appointment/Entity/WeeklySchedule.php` | `setting` JSON — محل ذخیره‌ی booking window (بدون فیلد جدید DB) |
| `src/Appointment/Service/SlotCalculatorService.php` | منطق محاسبه‌ی اسلات — اعمال window |
| `src/Appointment/Controller/AppointmentController.php` | `appointment-slots` (تک‌روز) — باید window را respect کند |
| `src/Appointment/Controller/AppointmentSettingsController.php` | weekly-schedule upsert + endpoint جدید month-availability |
| `src/Appointment/Repository/WeeklyScheduleRepository.php` | `findByDoctor` |
| `docs/api/appointment-settings.md` | مستندسازی window + endpoint جدید |
| `docs/api/appointment.md` | اگر رفتار `appointment-slots` تغییر کرد |
| `assets/admin/pages/DoctorDetailPage.tsx` | UI تنظیم booking window + toggle آنلاین |
## وضعیت فعلی (کد واقعی)
### `WeeklySchedule.php` — ذخیره‌سازی در JSON
```php
#[ORM\Column(type: 'json')]
private array $setting = [];
```
> `setting` الان فقط کلیدهای `"0".."6"` (روزهای هفته) را دارد. می‌توان یک کلید رزروشده‌ی غیرعددی مثل `"meta"` اضافه کرد بدون اینکه `SlotCalculatorService` که فقط کلیدهای عددی روز را می‌خواند، بشکند.
### `SlotCalculatorService::buildAllSessions()` — ترتیب اولویت فعلی
```php
private function buildAllSessions(Doctor $doctor, string $date): array
{
$dayStart = (int) strtotime($date . ' 00:00:00');
$dayEnd = $dayStart + 86400;
// 1. Blocked by holiday
if (!empty($this->holidayRepo->findActiveByDoctor($doctor, $dayStart, $dayEnd - 1))) {
return [];
}
// 2. Date override
foreach ($this->overrideRepo->findByDoctor($doctor) as $override) {
if (date('Y-m-d', $override->getDate()) === $date) {
if (!$override->isActive()) return [];
return $this->buildSessionsFromOverride($override->getSetting() ?? [], $dayStart);
}
}
// 3. Weekly schedule
$schedule = $this->scheduleRepo->findByDoctor($doctor);
if ($schedule === null) return [];
// ...
}
```
### `appointment-slots` controller — بدون چک window
```php
#[Route('/api/v1/appointment-slots', methods: ['GET'])]
public function slots(Request $request): JsonResponse
{
$doctorUuid = trim($request->query->get('doctor_uuid', ''));
$date = trim($request->query->get('date', ''));
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
// ... validation ...
$sessions = $this->slotCalculator->getAllSlotsWithAvailability($doctor, $date);
return $this->success([...]);
}
```
### `DoctorDetailPage.tsx` — schedule از قبل ذخیره می‌شود
```tsx
const saveMut = useMutation({
mutationFn: () => existing
? api.patch(`/api/v1/appointment-settings/weekly-schedule/${doctorUuid}`, { schedule: scheduleMap })
: api.post('/api/v1/appointment-settings/weekly-schedule', { doctor_uuid: doctorUuid, schedule: scheduleMap }),
});
```
## وظایف
اجرای مرحله‌به‌مرحله؛ بعد از هر قابلیت تست (PHP lint + migrate در صورت نیاز + admin build) و سپس commit جدا.
### ۱. مدل booking window در `WeeklySchedule.setting`
- ساختار جدید زیر کلید رزروشده `"meta"` (یا `"settings"`) در `setting`:
```json
{
"0": { "sessions": [...] },
"...": {},
"meta": {
"online_booking_enabled": true,
"booking_window_value": 2,
"booking_window_unit": "month"
}
}
```
- `booking_window_unit`: یکی از `"week"` یا `"month"`. `booking_window_value`: عدد مثبت (مثلاً ۲ هفته یا ۲ ماه).
- پیش‌فرض وقتی `meta` نیست: نوبت‌دهی آنلاین **روشن**، بازه‌ی پیش‌فرض (مثلاً ۱ ماه) — این پیش‌فرض را به‌صورت ثابت در سرویس تعریف کن و در پاسخ‌ها هم برگردان تا فرانت بداند.
- weekly-schedule controller (POST/PATCH) باید فیلدهای `meta` را در صورت ارسال بپذیرد و ذخیره کند (validation: `unit ∈ {week, month}`، `value ≥ 1`). کلیدهای عددی روز دست‌نخورده بمانند.
### ۲. اعمال window در `SlotCalculatorService`
- یک متد private مثل `isWithinBookingWindow(Doctor $doctor, string $date): bool` بساز:
- `meta` را از `schedule->getSetting()['meta']` بخوان (با fallback پیش‌فرض).
- اگر `online_booking_enabled === false` → خارج از بازه (false).
- سقف را با `strtotime("+{$value} {$unit}", today)` حساب کن؛ اگر `dayStart` بعد از سقف بود → false. تاریخ‌های گذشته هم false (همین حالا فرانت گذشته را می‌بندد ولی backend هم باید مقاوم باشد).
- در `buildAllSessions` **قبل از** بازگشت اسلات‌ها این چک را اعمال کن: اگر خارج از بازه → `return []`.
- توجه: تاریخ‌های یونیکس صحیح (نه DateTime object).
### ۳. endpoint عمومی month-availability
- متد جدید در `AppointmentSettingsController` با route:
`GET /api/v1/appointment-settings/month-availability/{doctorUuid}`**Permission: PUBLIC** (مثل `available-locations`).
- Query params: `year` و `month` (عددی). چون تقویم سایت شمسی است، **هر دو حالت را بپذیر**: اگر `calendar=jalali` آمد ورودی را شمسی تفسیر کن، در غیر این صورت میلادی. (یا ساده‌تر: همیشه میلادی بگیر و در پرامپت فرانت تبدیل شمسی→میلادی انجام شود — یکی را انتخاب کن و دقیق مستند کن.)
- برای هر روز ماه، با همان منطق `SlotCalculatorService` (holiday/override/window/weekly) تعیین کن آیا روز اسلات دارد یا نه. **برای پرهیز از سنگینی، یک متد سبک در سرویس اضافه کن** مثل `hasAnyAvailability(Doctor, string $date): bool` که فقط وجود session را چک کند (نه ساخت کامل اسلات‌ها).
- پاسخ:
```json
{
"success": true,
"data": {
"year": 2026,
"month": 6,
"disabled_dates": ["2026-06-16", "2026-06-17", "2026-06-20"],
"enabled_dates": ["2026-06-21", "2026-06-22"],
"online_booking_enabled": true,
"booking_window": { "value": 2, "unit": "month" }
}
}
```
> `disabled_dates`: روزهایی که تعطیل/خارج‌از‌بازه/بدون session‌اند (غیرقابل‌انتخاب). فرمت `Y-m-d`. فرانت این‌ها را روی تقویم خاکستری می‌کند.
### ۴. UI در پنل ادمین (`DoctorDetailPage.tsx`)
- در بخش تنظیمات برنامه‌ی هفتگی، یک کارت/سکشن «نوبت‌دهی آنلاین» اضافه کن:
- یک toggle برای `online_booking_enabled`.
- یک input عددی + select واحد (`هفته`/`ماه`) برای بازه.
- مقدار اولیه را از پاسخ `weekly-schedule` (`data?.data?.data?.meta`) بخوان (double-nested).
- در `saveMut` همان weekly-schedule، فیلد `meta` را هم به payload اضافه کن (`{ schedule: scheduleMap, meta: {...} }` یا داخل خود `scheduleMap` با کلید `meta`).
- از کلاس‌های CSS موجود همان صفحه استفاده کن؛ RTL؛ بدون کتابخانه‌ی جدید.
## نکات مهم
- **بدون migration:** booking window در `WeeklySchedule.setting` JSON می‌رود. اگر ترجیح می‌دهی فیلد مجزا در DB باشد، **متوقف شو و بپرس** (نیاز به migration دارد).
- همه‌ی controllerها از `BaseController`؛ پاسخ‌ها `$this->success()`/`$this->error()`. خطاها با `AppException(ErrorCodes::ERR_XXX, ...)`.
- `appointment-slots` تک‌روزه هم باید window را respect کند (وگرنه بیمار می‌تواند تاریخ خارج‌از‌بازه را مستقیم صدا بزند و اسلات بگیرد).
- `month-availability` باید **public** باشد (تقویم سایت قبل از لاگین لود می‌شود) — مثل `available-locations`.
- پاسخ weekly-schedule **double-nested** است (`data.data.data`) — UI ادمین با همین الگو می‌خواند.
- اولویت منطق دست‌نخورده بماند: holiday > override > window > weekly. window نباید روی اسلات‌های گذشته‌ی همین ماه تأثیر اشتباه بگذارد.
- بعد از تغییر API، **هم `docs/api/appointment-settings.md` و هم (در صورت تغییر رفتار) `docs/api/appointment.md`** را در همین session به‌روز کن: endpoint جدید با method/path/permission/query/response، و فیلدهای `meta`.
- تست: `ddev exec php -l ...` روی فایل‌های PHP؛ `ddev exec php bin/console debug:router | grep month-availability`؛ یک‌بار واقعی `curl` روی `month-availability` با پزشک تست `4a0594b1-008b-478a-a593-259b95d8c2dd`؛ و `ddev exec yarn dev` برای build پنل ادمین. سپس commit.