Files
clinicpro/docs/new_feture/taskes/task-03-resource-calendar/architecture.md
T
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
2026-07-30 11:43:58 +03:30

6.2 KiB

معماری — تسک ۰۳

ساختار فایل

src/Resource/
├── Entity/
│   ├── ResourceCalendar.php        # شیفت تکرارشوندهٔ هفتگی
│   └── ResourceException.php       # مرخصی/غیبت/سرویس دستگاه/تعطیلی موردی
├── Controller/
│   ├── ResourceCalendarController.php
│   └── ResourceExceptionController.php
├── Service/
│   ├── ResourceCalendarService.php
│   └── ResourceAvailabilityService.php   ← قلب این تسک
└── Repository/…

src/Holiday/
├── Entity/
│   ├── NationalHoliday.php
│   └── TenantHolidayOverride.php
├── Controller/HolidayController.php
├── Service/HolidayResolver.php
└── Command/ImportNationalHolidaysCommand.php

ResourceAvailabilityService — قرارداد

final class ResourceAvailabilityService
{
    /**
     * بازه‌های آزادِ خام یک منبع (بدون در نظر گرفتن نوبت‌ها).
     * خروجی: بازه‌های مرتب و ادغام‌شده، بر حسب Unix timestamp.
     *
     * @return array<array{start:int, end:int}>
     */
    public function rawWindows(ClinicResource $resource, int $from, int $to): array;

    /**
     * چرا این روز خالی است. null یعنی خالی نیست.
     * همان قرارداد SlotCalculatorService::explainEmptyDay — پنل به دلیل نیاز دارد.
     */
    public function explainEmptyDay(ClinicResource $resource, int $dayStart): ?string;

    public const EMPTY_NO_CALENDAR         = 'no_calendar';
    public const EMPTY_NATIONAL_HOLIDAY    = 'national_holiday';
    public const EMPTY_RESOURCE_EXCEPTION  = 'resource_exception';
    public const EMPTY_OUTSIDE_BRANCH_HOURS = 'outside_branch_hours';
    public const EMPTY_DAY_OFF             = 'day_off';
}

الگوریتم rawWindows

ورودی: منبع، [from, to)

۱. یک بار برای کل بازه واکشی کن (نه per-day):
     - branch_working_hours شعبهٔ منبع        (۱ کوئری)
     - resource_calendars منبع                (۱ کوئری)
     - resource_exceptions متداخل با بازه      (۱ کوئری)
     - national_holidays متداخل با بازه        (۱ کوئری)
     - tenant_holiday_overrides محیط           (۱ کوئری)

۲. برای هر روز از from تا to:
     الف) اگر تعطیل رسمی است و override با is_working=true ندارد → روز را رد کن
     ب) بازه‌های شیفت منبع آن روزِ هفته را بگیر
     ج) اگر شعبه ساعت کاری تعریف‌شده دارد → تقاطع بگیر
        اگر ندارد → بازهٔ منبع دست‌نخورده می‌ماند
     د) استثناهای متداخل را کسر کن (اتحاد استثناها، بعد تفاضل)
     ه) بازه‌های حاصل را به لیست اضافه کن

۳. ادغام بازه‌های مجاور و مرتب‌سازی

پنج کوئری ثابت برای هر بازه، نه رشد خطی با تعداد روز. این دقیقاً همان کاری است که SlotCalculatorService::findNextAvailableStart() امروز برای پزشک می‌کند و باید حفظ شود؛ تسک ۰۶ روی همین حساب می‌کند که بتواند زیر نیم ثانیه بماند.

عملیات بازه — یک جای واحد

تقاطع، اتحاد، تفاضل و ادغامِ بازه‌ها در سه تسک بعدی هم لازم است. یک کلاس بدون وابستگی:

// src/Shared/Time/IntervalSet.php
final class IntervalSet
{
    /** @param array<array{start:int,end:int}> $intervals */
    public static function normalize(array $intervals): array;   // مرتب + ادغام مجاور
    public static function intersect(array $a, array $b): array;
    public static function subtract(array $from, array $minus): array;
    public static function union(array $a, array $b): array;
    public static function totalSeconds(array $intervals): int;
}

خالص و بدون I/O → تست واحد سریع و بدون دیتابیس. هر جای دیگری که بازه جمع/کم می‌کند باید از این استفاده کند، وگرنه سه پیاده‌سازی با سه باگ مرزی متفاوت خواهیم داشت.

تعطیلات رسمی

class NationalHoliday          // سراسری — GlobalTables::ENTITIES
{
    private int     $date;         // نیمه‌شب روز، Unix
    private string  $title;        // «عید فطر»
    private bool    $isOfficial;   // تعطیل رسمی یا مناسبت غیرتعطیل
}

class TenantHolidayOverride    // per محیط
{
    use TenantOwnedTrait;
    private int  $date;
    private bool $isWorking;       // true = این تعطیل رسمی برای ما کاری است
    private ?string $note;
}

HolidayResolver::isClosedFor(EntityContext $ctx, int $dayStart): bool تنها نقطهٔ ترکیب این دو است. Holiday موجود (per پزشک) دست‌نخورده می‌ماند و در حالت slot/service همچنان مرجع است؛ در حالت resource هر دو منبع اعمال می‌شوند (اتحاد).

import تعطیلات

ddev exec php bin/console app:holiday:import --year=1405 --file=var/holidays-1405.json

فایل JSON با تاریخ شمسی؛ تبدیل با jalaali-js معادل PHP در src/Shared/Time/JalaliDate.php (اگر نبود، بساز). عمداً از سرویس آنلاین نمی‌خوانیم: تعطیلات ایران سالانه با مصوبه تغییر می‌کنند و وابستگی به یک API خارجی یعنی جستجوی وقت به آن گره می‌خورد.

پنل ادمین

  • ResourceCalendarPage.tsx — گرید هفت‌روزه، هر روز چند بازه، درگ ندارد (فرم ساده)
  • ResourceExceptionsPage.tsx — لیست + PersianDatePicker برای بازه
  • HolidaysSettingsPage.tsx — تعطیلات رسمی سال با تیک «ما این روز کار می‌کنیم»
  • هر سه زیرصفحه‌اند → backTo اجباری