Files
clinicpro/.claude/prompt/doctor-booking-window-and-month-availability.md
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

12 KiB
Raw Permalink Blame History

تنظیمات نوبت‌دهی آنلاین دکتر: بازه‌ی رزرو (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

#[ORM\Column(type: 'json')]
private array $setting = [];

setting الان فقط کلیدهای "0".."6" (روزهای هفته) را دارد. می‌توان یک کلید رزروشده‌ی غیرعددی مثل "meta" اضافه کرد بدون اینکه SlotCalculatorService که فقط کلیدهای عددی روز را می‌خواند، بشکند.

SlotCalculatorService::buildAllSessions() — ترتیب اولویت فعلی

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

#[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 از قبل ذخیره می‌شود

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:
{
  "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 را چک کند (نه ساخت کامل اسلات‌ها).
  • پاسخ:
{
  "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.