# دیتابیس — تسک ۰۳ ## `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 | |