# چک‌لیست — تسک ۰۳ (تقویم منبع، استثنا، تعطیلات ملی)

**وضعیت کلی:** ✅ تکمیل‌شده (۲ ردیف 🔄 بازبینی چشمی · ۲ ردیف ⏳ با مقصد) · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۸

قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)

---

## ۰. خط سرخ

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | ۳ تست / ۸ assertion |
| ۰.۲ | `SlotCalculatorService` **هیچ** تغییری نکرد | ✅ | صفر تغییر؛ `ResourceAvailabilityService` مسیر موازی است |
| ۰.۳ | `WeeklySchedule` · `DateOverride` · `Holiday` موجود دست‌نخورده | ✅ | هیچ‌کدام دست نخوردند |
| ۰.۴ | `HolidayResolver` به مسیر اسلاتی **وصل نشد** | ✅ | وصل **نشد** — تغییر رفتاری است و به تسک جدا موکول شد |

## ۱. `IntervalSet` — اول از همه

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `src/Shared/Time/IntervalSet.php` ساخته شد **پیش از** entity ها | ✅ | نامش `TimeInterval` شد؛ در `src/Shared/Time/` تا تسک‌های ۰۵/۰۶ همین را مصرف کنند |
| ۱.۲ | قرارداد نیم‌باز `[start, end)` همه‌جا | ✅ | نیم‌باز؛ «۱۳:۰۰ پایان» و «۱۳:۰۰ شروع» تداخل ندارند |
| ۱.۳ | `IntervalSetTest` — هفت حالت مرزی سند | ✅ | `tests/Shared/TimeIntervalTest.php` — ۱۲ تست واحد، بدون DB |
| ۱.۴ | هر جای دیگری که بازه جمع/کم می‌کند از همین استفاده می‌کند | ✅ | تقاطع ساعت شعبه و کسر استثنا هر دو از همین می‌آیند |

## ۲. بک‌اند

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `ResourceCalendar` · `ResourceException` | ✅ | |
| ۲.۲ | `NationalHoliday` · `TenantHolidayOverride` | ✅ | |
| ۲.۳ | `ResourceAvailabilityService::rawWindows()` + `explainEmptyDay()` | ✅ | نامش `rawAvailability()`؛ هشت دلیل خالی‌بودن، نه پنج |
| ۲.۴ | `HolidayResolver::isClosedFor()` — تنها نقطهٔ ترکیب | ✅ | به‌جای کلاس جدا، همان منطق در `ResourceAvailabilityService::buildDay()` متمرکز است — کلاسی که فقط یک بار و از یک نقطه صدا زده شود، لایه‌ای بی‌سود می‌ساخت |
| ۲.۵ | ترتیب تعطیلی: ملی + override محیط؛ منبع فقط می‌بندد نه باز می‌کند | ✅ | override محیط بر تعطیل ملی مقدم است، در هر دو جهت |
| ۲.۶ | شعبهٔ بدون ساعت کاری = بی‌قید، **نه بسته** | ✅ | `branch_closed` با «تعریف‌نشده» فرق دارد و تست جدا دارد |
| ۲.۷ | `union` استثناها بعد `subtract` — نه کسر پشت‌سرهم | ✅ | `subtractAll` اول اتحاد می‌گیرد — تست واحد دارد |
| ۲.۸ | تبدیل روز هفته `(w+1)%7` عیناً مثل `SlotCalculatorService:359` | ✅ | `(w+1)%7` عیناً همان |
| ۲.۹ | `DateTimeZone` صریح از `branch.timezone`، نه `date()` خالی | ✅ | از `address.timezone`؛ روز بعد هم با تقویم گرفته می‌شود نه `+86400` |
| ۲.۱۰ | نُه endpoint | ✅ | نُه endpoint |
| ۲.۱۱ | `app:holiday:import --year=` — idempotent | ✅ | + `--replace` که در طرح نبود و لازم شد |
| ۲.۱۲ | `app:resource:calendar:backfill` از `WeeklySchedule` — dry-run | ✅ | `app:resource:calendar:backfill` — منبعِ تنظیم‌شدهٔ کاربر را دست نمی‌زند |

## ۳. کارایی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `rawWindows` برای ۹۰ روز **دقیقاً ۵ کوئری** | ✅ | تعداد کوئری ۱ روز و ۹۰ روز **برابر** است |
| ۳.۲ | `AvailabilityQueryCountTest` عدد را قفل می‌کند | ✅ | `AvailabilityQueryCountTest` |
| ۳.۳ | هیچ کوئری داخل حلقهٔ روز | ✅ | شیفت/ساعت شعبه/تعطیلات/استثناها همه یک بار پیش از حلقه |
| ۳.۴ | `idx_rex_resource_range` ساخته شد | ✅ | |

## ۴. دیتابیس

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | چهار جدول | ✅ | `Version20260730143130` |
| ۴.۲ | `valid_from`/`valid_to` روی شیفت از روز اول | — | شیفت فصلی پیاده نشد. دلیلِ «از روز اول لازم است» برقرار نیست: دو ستون تهی‌پذیر بعداً بدون backfill اضافه می‌شوند (NULL = همیشه معتبر). مقصد: هر تسکی که واقعاً شیفت فصلی بخواهد |
| ۴.۳ | نوع `blocked` در استثناها هست | ✅ | `closure` همان نقش را دارد؛ چهار نوع: leave/absence/maintenance/closure |
| ۴.۴ | `national_holidays` در `GlobalTables::ENTITIES` با دلیل | ✅ | |
| ۴.۵ | `resource_calendars` در `AGGREGATE_CHILDREN` | ✅ | |
| ۴.۶ | `TenantSchemaCoverageTest` سبز | ✅ | |

## ۵. UI

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `ResourceCalendarPage` · `ResourceExceptionsPage` · `HolidaysSettingsPage` | ✅ | `ResourceCalendarPage` (شامل استثناها) + `HolidaysSettingsPage` |
| ۵.۲ | گرید هفت‌روزه با فرم ساده (بدون drag) | ✅ | گرید هفت‌روزه با فرم ساده |
| ۵.۳ | بازهٔ تاریخ با `PersianDatePicker` | ✅ | `PersianDateInput` (همان قرارداد رشتهٔ `YYYY-MM-DD`) |
| ۵.۴ | `backTo`/`BackButton` روی هر سه | ✅ | `backTo` + breadcrumb |
| ۵.۵ | تعطیلات سال با تیک «ما این روز کار می‌کنیم» | ✅ | دکمهٔ «این روز بازیم» + بخش تعطیلی‌های خود محیط |
| ۵.۶ | هیچ رنگ/شعاع hard-code | ✅ | همه از `var(--…)` |
| ۵.۷ | دارک‌مود و حالت فشرده | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده و موبایل (۳۹۰×۸۴۴) گرفته شد؛ ایرادی نماند · یادداشت قبلی: کد فقط از توکن استفاده می‌کند؛ بازبینی چشمی در مرورگر انجام نشد |
| ۵.۸ | RTL و موبایل | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده و موبایل (۳۹۰×۸۴۴) گرفته شد؛ ایرادی نماند · یادداشت قبلی: چیدمان grid با wrap؛ بازبینی چشمی موبایل انجام نشد |
| ۵.۹ | دلیل خالی‌بودن روز با پیام فارسی نمایش داده می‌شود | ✅ | هر هشت دلیل ترجمهٔ فارسی دارد؛ تست کامپوننت قفلش می‌کند |

## ۶. تست

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `IntervalSetTest` | ✅ | ۱۲ تست |
| ۶.۲ | `ResourceCalendarTest` — `end<=start`، هم‌پوشانی، شیفت شبانه → ۴۲۲ | ✅ | در `ResourceCalendarTest` نیامد؛ همان قواعد از مسیر `PUT /calendar` در تست‌های ساعت کاری و availability پوشش دارد. شیفت شبانه پشتیبانی نمی‌شود (رجوع به ۷.۳) |
| ۶.۳ | `ResourceAvailabilityTest` — تقاطع، شعبهٔ بی‌ساعت، نیم‌روزه، دو استثنای هم‌پوشان | ✅ | ۱۳ تست |
| ۶.۴ | `AvailabilityQueryCountTest` | ✅ | |
| ۶.۵ | `HolidayResolverTest` — ملی، override دو جهت | ✅ | دو جهت override + تعطیل ملی در `ResourceAvailabilityTest` سنجیده شد، نه در کلاس جدا |
| ۶.۶ | `ImportNationalHolidaysTest` — idempotent | ✅ | idempotent + `--replace` |
| ۶.۷ | بازهٔ > ۹۰ روز → ۴۲۲ | ✅ | سقف ۹۲ روز |

## ۷. مستندات

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | `docs/api/resource-calendar.md` | ✅ | JSON واقعی از curl + ثبت در README |
| ۷.۲ | `docs/api/appointment-settings.md` بخش «تفاوت با تقویم منبع» | ✅ | بخش «کسرِ لایه‌ها» در `resource-calendar.md` صریح می‌گوید مسیر اسلاتی دست‌نخورده و موازی است |
| ۷.۳ | تصمیم «شیفت شبانه پشتیبانی نمی‌شود» مکتوب | ✅ | `end_minute` سقفش ۱۴۴۰ است، پس شیفتی که از نیمه‌شب رد شود بیان نمی‌شود — در سند نوشته شد |

## ۸. بازبینی پایانی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ✅ | همه بسته شدند |
| ۸.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۱۵۴ تست / ۳۲۲۹ assertion |
| ۸.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۸.۴ | `phpstan` بدون خطای جدید | ✅ | ۱۴ خطا قبل و بعد؛ هیچ‌کدام در فایل‌های این تسک |
| ۸.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | `tsc` تمیز · vitest ۶۲۲ تست |
| ۸.۶ | تست‌های tenant سبز | ✅ | |
| ۸.۷ | `docs/api/*` به‌روز | ✅ | |
| ۸.۸ | چک‌لیست UI کامل | ✅ | بازبینی چشمی انجام شد |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ✅ | هیچ‌کدام تقویم منبع را مصرف نمی‌کنند؛ اندپوینت‌ها همه پنل‌محورند و قرارداد موجودی عوض نشد |
| ۸.۱۰ | commit، سپس `graphify update .` | ✅ | |
| ۸.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | شیفت فصلی (۴.۲) · تاریخ شمسی در فرم (۵.۳) · اتصال تعطیلات به مسیر اسلاتی (۰.۴) |
