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` اجباری
|
||||
@@ -0,0 +1,112 @@
|
||||
# دیتابیس — تسک ۰۳
|
||||
|
||||
## `resource_calendars`
|
||||
|
||||
شیفت تکرارشوندهٔ هفتگی یک منبع.
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `resource_id` | INT NOT NULL | FK → `clinic_resources.id` ON DELETE CASCADE |
|
||||
| `day_of_week` | TINYINT NOT NULL | ۰=شنبه … ۶=جمعه |
|
||||
| `sequence` | TINYINT NOT NULL DEFAULT 0 | |
|
||||
| `start_minute` | SMALLINT NOT NULL | |
|
||||
| `end_minute` | SMALLINT NOT NULL | |
|
||||
| `valid_from` | INT NULL | شیفت فصلی؛ NULL = از همیشه |
|
||||
| `valid_to` | INT NULL | NULL = تا همیشه |
|
||||
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
|
||||
```sql
|
||||
UNIQUE KEY uniq_rc_resource_day_seq (resource_id, day_of_week, sequence)
|
||||
KEY idx_rc_resource_day (resource_id, day_of_week, active)
|
||||
```
|
||||
|
||||
`valid_from/valid_to` از روز اول: شیفت تابستانی/زمستانی حالت رایج کلینیک است و
|
||||
افزودنش بعداً یعنی یا حذف و ثبت دوبارهٔ شیفتها یا یک جدول موازی.
|
||||
|
||||
فرزند aggregate با ریشهٔ `ClinicResource`.
|
||||
|
||||
## `resource_exceptions`
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `uuid` | VARCHAR(36) UNIQUE | **از request میآید** → جفت tenant خودش را دارد |
|
||||
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | از منبع مشتق میشود |
|
||||
| `resource_id` | INT NOT NULL | FK ON DELETE CASCADE |
|
||||
| `type` | VARCHAR(20) NOT NULL | `leave` \| `absence` \| `maintenance` \| `blocked` |
|
||||
| `start_at` | INT NOT NULL | Unix |
|
||||
| `end_at` | INT NOT NULL | Unix، `> start_at` |
|
||||
| `reason` | VARCHAR(255) NULL | |
|
||||
| `created_by` | INT NULL | FK → `users.id` ON DELETE SET NULL |
|
||||
| `created_at` | INT NOT NULL | |
|
||||
|
||||
```sql
|
||||
KEY idx_rex_tenant (entity_type, entity_id, start_at)
|
||||
KEY idx_rex_resource_range (resource_id, start_at, end_at)
|
||||
```
|
||||
|
||||
`idx_rex_resource_range` کوئری داغ است: «استثناهای این منبع که با [from, to) تداخل دارند».
|
||||
|
||||
> نوع `blocked` عمداً هست: مسدود کردن دستیِ یک بازه توسط منشی («امروز عصر کسی را نگذار»)
|
||||
> با مرخصی یکی نیست و گزارش بهرهوری (تسک ۱۴) باید تفکیکشان کند.
|
||||
|
||||
## `national_holidays`
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `date` | INT NOT NULL UNIQUE | نیمهشب روز به وقت `Asia/Tehran`، Unix |
|
||||
| `jalali_date` | VARCHAR(10) NOT NULL | `1405-01-13` — برای import و نمایش |
|
||||
| `title` | VARCHAR(150) NOT NULL | |
|
||||
| `is_official` | TINYINT(1) NOT NULL DEFAULT 1 | مناسبت غیرتعطیل هم ثبت میشود |
|
||||
| `created_at` | INT NOT NULL | |
|
||||
|
||||
```sql
|
||||
UNIQUE KEY uniq_national_holiday_date (date)
|
||||
KEY idx_national_holiday_jalali (jalali_date)
|
||||
```
|
||||
|
||||
سراسری — در `GlobalTables::ENTITIES` با دلیل: «تقویم رسمی کشور، مال هیچ محیطی نیست».
|
||||
|
||||
## `tenant_holiday_overrides`
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `uuid` | VARCHAR(36) UNIQUE | |
|
||||
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
|
||||
| `date` | INT NOT NULL | نیمهشب روز |
|
||||
| `is_working` | TINYINT(1) NOT NULL | true = روز رسمیِ تعطیل، برای ما کاری است |
|
||||
| `note` | VARCHAR(255) NULL | |
|
||||
| `created_at` | INT NOT NULL | |
|
||||
|
||||
```sql
|
||||
UNIQUE KEY uniq_tho_tenant_date (entity_type, entity_id, date)
|
||||
KEY idx_tho_tenant (entity_type, entity_id, date)
|
||||
```
|
||||
|
||||
`is_working=false` هم معنی دارد: روزی که رسمی نیست ولی این محیط تعطیل است (مثلاً
|
||||
تعطیلی سالانهٔ کلینیک). پس این جدول هر دو جهت را میپوشاند و `Holiday` موجود فقط برای
|
||||
سازگاری عقبرو در حالت `slot`/`service` میماند.
|
||||
|
||||
## Migration
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
ddev exec php bin/console app:holiday:import --year=1405 --file=var/holidays-1405.json
|
||||
```
|
||||
|
||||
`app:resource:calendar:backfill` (اختیاری، در همین تسک): برای هر منبعِ `type=doctor` که از
|
||||
`WeeklySchedule` ساخته شده، شیفتهای همان برنامه را به `resource_calendars` کپی میکند تا
|
||||
تقویم منبع از روز اول خالی نباشد. dry-run پیشفرض.
|
||||
|
||||
## طبقهبندی tenant
|
||||
|
||||
| جدول | وضعیت | دلیل |
|
||||
|---|---|---|
|
||||
| `resource_calendars` | `AGGREGATE_CHILDREN` | ریشه `ClinicResource`؛ uuid از request نمیگیرد |
|
||||
| `resource_exceptions` | جفت tenant | uuid از request میآید (`PATCH /resource-exception/{uuid}`) |
|
||||
| `national_holidays` | `ENTITIES` | تقویم کشوری |
|
||||
| `tenant_holiday_overrides` | جفت tenant | |
|
||||
@@ -0,0 +1,133 @@
|
||||
# نکات پیادهسازی — تسک ۰۳
|
||||
|
||||
## ۱. `IntervalSet` اول، بقیه بعد
|
||||
|
||||
اولین چیزی که مینویسی `src/Shared/Time/IntervalSet.php` و تستش است — قبل از هر entity.
|
||||
سه تسک بعدی روی درستیاش حساب میکنند و باگ مرزی در تفاضل بازه، در تسک ۰۶ به شکل
|
||||
«یک اسلات عجیب» ظاهر میشود که دیباگش ساعتها میبرد.
|
||||
|
||||
مرزهایی که تست واحد باید بپوشاند:
|
||||
|
||||
```php
|
||||
subtract([[0,100]], [[0,100]]) === [] // کامل
|
||||
subtract([[0,100]], [[20,30]]) === [[0,20],[30,100]] // وسط
|
||||
subtract([[0,100]], [[100,200]]) === [[0,100]] // مجاور، نه متداخل
|
||||
subtract([[0,100]], [[-10,10]]) === [[10,100]] // از چپ بیرونزده
|
||||
intersect([[0,100]], []) === [] // خالی = هیچ، نه همهچیز
|
||||
normalize([[0,50],[50,100]]) === [[0,100]] // ادغام مجاور
|
||||
normalize([[10,20],[0,5]]) === [[0,5],[10,20]] // مرتبسازی
|
||||
```
|
||||
|
||||
قرارداد بازهها: **نیمباز `[start, end)`**. همهجا. `end == start` یعنی بازهٔ تهی و حذف میشود.
|
||||
|
||||
## ۲. شعبهٔ بدون ساعت کاری = بیقید، نه بسته
|
||||
|
||||
```php
|
||||
$branchHours = $this->branchHoursRepo->forDay($branch, $dow);
|
||||
$windows = $branchHours === []
|
||||
? $resourceShifts // بیقید
|
||||
: IntervalSet::intersect($resourceShifts, $branchHours);
|
||||
```
|
||||
|
||||
اگر برعکسش را بنویسی، همهٔ دادههای موجود (که هیچ شعبهای ساعت کاری ندارد) یکشبه
|
||||
هیچ وقتی نمیدهند. این تصمیم در تسک ۰۱ هم نوشته شده — هر دو جا باید یکی باشد.
|
||||
|
||||
## ۳. تعطیلات: ملی، محیطی، منبعی — ترتیب
|
||||
|
||||
```
|
||||
روز تعطیل است اگر:
|
||||
(در national_holidays با is_official=true باشد
|
||||
و tenant_holiday_override با is_working=true نداشته باشد)
|
||||
یا
|
||||
(tenant_holiday_override با is_working=false داشته باشد)
|
||||
```
|
||||
|
||||
منبع میتواند با `resource_exception` روزِ باز را ببندد، ولی **نمیتواند** روز تعطیل را باز
|
||||
کند. باز کردن فقط در سطح محیط معنی دارد (کل کلینیک آن روز کار میکند یا نه).
|
||||
|
||||
## ۴. زمان و منطقهٔ زمانی
|
||||
|
||||
ذخیرهسازی همیشه Unix timestamp (UTC ذاتی). `date('w', $ts)` و `strtotime('today')` به
|
||||
منطقهٔ زمانی PHP وابستهاند. `SlotCalculatorService` امروز روی همین فرض کار میکند و
|
||||
`php.ini` پروژه روی `Asia/Tehran` است.
|
||||
|
||||
قاعده: **هیچجا `date()` بدون منطقهٔ زمانی صریح ننویس** وقتی شعبه `timezone` دارد.
|
||||
|
||||
```php
|
||||
$tz = new \DateTimeZone($branch->getTimezone());
|
||||
$day = (new \DateTimeImmutable("@{$ts}"))->setTimezone($tz);
|
||||
$dow = ((int) $day->format('w') + 1) % 7; // ← همان تبدیل SlotCalculatorService
|
||||
```
|
||||
|
||||
تبدیل `(w + 1) % 7` عمداً همان است که در `SlotCalculatorService:359` هست. دو قرارداد
|
||||
شمارش روز هفته در یک کدبیس = باگ قطعی.
|
||||
|
||||
## ۵. کارایی — پنج کوئری، نه پنج × تعداد روز
|
||||
|
||||
```php
|
||||
// ❌ اشتباه
|
||||
foreach ($days as $day) { $exceptions = $repo->findForDay($resource, $day); }
|
||||
|
||||
// ✅ درست — یک بار برای کل بازه، بعد در حافظه
|
||||
$exceptions = $repo->findOverlapping($resource, $from, $to);
|
||||
$byDay = $this->bucketByDay($exceptions, $from, $to);
|
||||
```
|
||||
|
||||
معیار: `rawWindows()` برای یک منبع در ۹۰ روز باید **دقیقاً ۵ کوئری** بزند. یک تست با
|
||||
`ProfilerStack` یا شمارندهٔ `SQLLogger` این را قفل کند — وگرنه اولین refactor آن را میشکند.
|
||||
|
||||
## ۶. سازگاری با نوبتدهی موجود
|
||||
|
||||
این تسک هیچچیز از `SlotCalculatorService` را تغییر نمیدهد. `ResourceAvailabilityService`
|
||||
یک سرویس **موازی** است که فقط در حالت `booking_mode=resource` (تسک ۰۶) صدا زده میشود.
|
||||
|
||||
تنها نقطهٔ اتصال: `HolidayResolver` میتواند از تسک بعد در `SlotCalculatorService` هم
|
||||
استفاده شود تا پزشکهای حالت `slot` هم تعطیلات رسمی را بگیرند. آن یک تغییر رفتاری است
|
||||
(روزهایی که امروز باز بودند بسته میشوند) — پس **در این تسک انجام نده**، بهعنوان یک
|
||||
تغییر جدا با تأیید محصول.
|
||||
|
||||
## ۷. edge case ها
|
||||
|
||||
| حالت | رفتار درست |
|
||||
|---|---|
|
||||
| منبع بدون هیچ `resource_calendar` | `rawWindows` خالی، `explainEmptyDay` = `no_calendar` |
|
||||
| استثنا که کل روز را میپوشاند | آن روز خالی |
|
||||
| استثنا نیمروزه | فقط همان بازه کسر شود |
|
||||
| دو استثنای همپوشان | `union` بعد `subtract` — نه کسر پشتسرهم (دوبار کسر بازهٔ مشترک) |
|
||||
| شیفت با `valid_to` گذشته | نادیده گرفته شود |
|
||||
| شیفت شبانه (۲۲:۰۰ تا ۰۲:۰۰) | **پشتیبانی نمیشود در این تسک** — `422` با پیام روشن. دو ردیف بنویسند |
|
||||
| `from > to` | `422` |
|
||||
| بازهٔ بزرگتر از ۹۰ روز | `422` — سقف مستند بند ۱۰ |
|
||||
| تعطیل رسمی + override محیط + استثنای منبع، هر سه روی یک روز | ترتیب بند ۳ بالا |
|
||||
|
||||
شیفت شبانه عمداً بیرون است: پشتیبانیاش یعنی هر بازهٔ روزانه ممکن است به روز بعد سرریز
|
||||
کند و کل منطق bucket-by-day باید بازنویسی شود. اگر کلینیکی لازم داشت، تسک جدا.
|
||||
|
||||
## ۸. تست
|
||||
|
||||
```
|
||||
tests/Shared/Time/IntervalSetTest.php ← اول این، واحد و بدون DB
|
||||
tests/Resource/ResourceCalendarTest.php
|
||||
- PUT شیفتها، بازخوانی یکسان
|
||||
- end <= start → 422 · همپوشانی در یک روز → 422
|
||||
- شیفت شبانه → 422
|
||||
tests/Resource/ResourceAvailabilityTest.php
|
||||
- منبع بدون تقویم → خالی + دلیل no_calendar
|
||||
- تقاطع با ساعت شعبه
|
||||
- شعبهٔ بدون ساعت → بیقید
|
||||
- کسر استثنای نیمروزه
|
||||
- دو استثنای همپوشان → یک بار کسر
|
||||
tests/Resource/AvailabilityQueryCountTest.php
|
||||
- ۹۰ روز → دقیقاً ۵ کوئری
|
||||
tests/Holiday/HolidayResolverTest.php
|
||||
- تعطیل رسمی برای همه
|
||||
- override is_working=true فقط برای همان محیط
|
||||
- override is_working=false روی روز غیررسمی
|
||||
tests/Holiday/ImportNationalHolidaysTest.php
|
||||
- idempotent
|
||||
```
|
||||
|
||||
## ۹. مستندات
|
||||
|
||||
`docs/api/resource-calendar.md` بساز. در `docs/api/appointment-settings.md` یک بخش
|
||||
«تفاوت با تقویم منبع» اضافه کن تا کسی دو سیستم را قاطی نکند.
|
||||
@@ -0,0 +1,76 @@
|
||||
# تسک ۰۳ — تقویم منبع، استثنا، تعطیلات ملی
|
||||
|
||||
**فاز:** ۱ (هسته) · **وابستگی:** ۰۱، ۰۲ · **زمان:** ۱۲-۱۴ ساعت
|
||||
|
||||
---
|
||||
|
||||
## هدف
|
||||
|
||||
مستند بند ۹ ساعت آزاد را از کسر هفت لایه میسازد. امروز چهار لایه داریم و همه روی
|
||||
**پزشک** سوارند. این تسک لایههای غایب را اضافه میکند و آنها را روی **منبع** مینشاند:
|
||||
|
||||
```
|
||||
ساعت کاری شعبه ← تسک ۰۱ ساخت، اینجا وارد محاسبه میشود
|
||||
– شیفت منبع ← این تسک
|
||||
– تعطیلات رسمی کشور ← این تسک
|
||||
– مرخصی و غیبت ← این تسک
|
||||
– سرویس دورهای دستگاه ← این تسک (همان جدول استثنا با نوع دیگر)
|
||||
– نوبتهای ثبتشده ← موجود (تسک ۰۷ به resource_occupancy منتقل میکند)
|
||||
– رزروهای موقت ← موجود
|
||||
– آمادهسازی و تمیزکاری ← تسک ۰۲ ستونها را ساخت، تسک ۰۶ اعمال میکند
|
||||
```
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
- `WeeklySchedule` — JSON هفتگی per `(doctor, clinic)` با `sessions[]`
|
||||
- `DateOverride` — تنظیم یک روز خاص per `(doctor, clinic)`
|
||||
- `Holiday` — بازهٔ تعطیلی per `(doctor, clinic)`، دستی
|
||||
- جدول تعطیلات رسمی کشور **وجود ندارد**؛ هر پزشک باید دستی ثبت کند
|
||||
|
||||
## دامنه
|
||||
|
||||
**هست:** `ResourceCalendar` (شیفت تکرارشوندهٔ منبع)، `ResourceException` (مرخصی، غیبت،
|
||||
سرویس دورهای، تعطیلی موردی)، `NationalHoliday` (جدول کشوری با import شمسی) و
|
||||
`TenantHolidayOverride` (کلینیکی که پنجشنبه کار میکند)، و یک سرویس واحد
|
||||
`ResourceAvailabilityService` که «ساعت آزاد یک منبع در یک بازه» را میدهد.
|
||||
|
||||
**نیست:** تقاطع چند منبع و برنامهٔ چندبخشی (تسک ۰۶)، ثبت اشغال (تسک ۰۷).
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | توضیح |
|
||||
|---|---|---|
|
||||
| GET | `/api/v1/resource/{uuid}/calendar` | شیفتهای هفتگی منبع |
|
||||
| PUT | `/api/v1/resource/{uuid}/calendar` | جایگزینی کامل شیفتها |
|
||||
| GET | `/api/v1/resource/{uuid}/exceptions` | مرخصی/سرویس، با فیلتر بازه |
|
||||
| POST | `/api/v1/resource/{uuid}/exception` | ثبت استثنا |
|
||||
| PATCH/DELETE | `/api/v1/resource-exception/{uuid}` | |
|
||||
| GET | `/api/v1/resource/{uuid}/availability?from&to` | ساعت آزاد خام (بدون نوبت) — برای پنل |
|
||||
| GET | `/api/v1/national-holidays?year=1405` | تعطیلات رسمی سال |
|
||||
| POST | `/api/v1/holiday-overrides` | تغییر تعطیلی رسمی توسط محیط |
|
||||
| DELETE | `/api/v1/holiday-override/{uuid}` | |
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ موفق: منبع «اپراتور مریم» شیفت شنبه تا چهارشنبه ۹:۰۰-۱۷:۰۰ میگیرد →
|
||||
`GET /resource/{uuid}/availability?from=…&to=…` پنج بازه برمیگرداند و جمعه خالی است.
|
||||
- ✅ موفق: مرخصی سهشنبه ثبت میشود → همان endpoint سهشنبه را خالی میدهد.
|
||||
- ✅ موفق: تعطیل رسمی ۱۳ فروردین در `national_holidays` هست → آن روز برای همهٔ منابع
|
||||
همهٔ محیطها خالی است، بدون هیچ ثبت دستی.
|
||||
- ✅ موفق: کلینیکی که پنجشنبه کار میکند یک `holiday_override` با `is_working=true` ثبت
|
||||
میکند → فقط منابع همان محیط پنجشنبه باز میشوند.
|
||||
- ❌ خطا: شیفت با `end_minute <= start_minute` → `422`.
|
||||
- ❌ خطا: استثنا با `end < start` → `422`؛ استثنا روی منبع محیط دیگر → `404`.
|
||||
- ⚠️ مرزی: شیفت منبع بیرون از ساعت کاری شعبه → **تقاطع** گرفته میشود، نه رد. اگر تقاطع
|
||||
خالی شد، پاسخ `availability` آن روز را خالی میدهد و دلیل `outside_branch_hours` را
|
||||
برمیگرداند.
|
||||
- ⚠️ مرزی: شعبهٔ بدون ساعت کاری → شیفت منبع بیقید اعمال میشود (سازگاری با دادههای موجود).
|
||||
- ⚠️ مرزی: استثنای نیمروزه (۱۴:۰۰ تا ۱۸:۰۰) → فقط همان بازه کسر میشود، نه کل روز.
|
||||
- ⚠️ مرزی: دو استثنای همپوشان → مجاز، اتحاد گرفته میشود.
|
||||
|
||||
## خروجی
|
||||
|
||||
- `src/Resource/Calendar/` + `src/Resource/Service/ResourceAvailabilityService.php`
|
||||
- صفحات: `ResourceCalendarPage.tsx`, `ResourceExceptionsPage.tsx`, `HolidaysSettingsPage.tsx`
|
||||
- `docs/api/resource-calendar.md`
|
||||
- دستور `app:holiday:import --year=1405` برای بارگذاری تعطیلات رسمی
|
||||
Reference in New Issue
Block a user