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.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# معماری — تسک ۰۳
|
||||
|
||||
## ساختار فایل
|
||||
|
||||
```
|
||||
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` — قرارداد
|
||||
|
||||
```php
|
||||
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()` امروز برای پزشک میکند و باید حفظ شود؛
|
||||
تسک ۰۶ روی همین حساب میکند که بتواند زیر نیم ثانیه بماند.
|
||||
|
||||
## عملیات بازه — یک جای واحد
|
||||
|
||||
تقاطع، اتحاد، تفاضل و ادغامِ بازهها در سه تسک بعدی هم لازم است. یک کلاس بدون وابستگی:
|
||||
|
||||
```php
|
||||
// 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 → تست واحد سریع و بدون دیتابیس. هر جای دیگری که بازه جمع/کم میکند
|
||||
باید از این استفاده کند، وگرنه سه پیادهسازی با سه باگ مرزی متفاوت خواهیم داشت.
|
||||
|
||||
## تعطیلات رسمی
|
||||
|
||||
```php
|
||||
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 تعطیلات
|
||||
|
||||
```bash
|
||||
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` اجباری
|
||||
Reference in New Issue
Block a user