Writing the query-count test that task 14 owed showed the growth was real: one resource cost 10 queries, six cost 33 — about five per resource, because the available-minutes figure walked each resource's calendar on its own. Holidays, tenant overrides and branch hours are identical for every resource in a report, so they now load once outside the loop; shifts and exceptions load for all resources in one query each. The batched path is a new method rather than a change to rawAvailability, which the booking engine also calls. The test pins the shape of the growth, not an exact count. Also landed: - app:segment:seed-templates with beauty, dental and physio presets. Building four segments and their requirements by hand is the first thing a new clinic must do and the most tedious; this gives them something to edit instead of an empty page. It refuses to touch a service that already has segments unless --force, and it will not invent resource types the tenant never defined. - book-all is all-or-nothing, proven rather than asserted: with a calendar open one day a week and a 1-2 day protocol gap, session one finds a slot and session two cannot, and every session must come back planned. - credit_refundable: false takes the credit back with a negative adjustment and deletes nothing — the ledger stays append-only. - the segments editor has frontend tests, including that it sends back what the user sees and renders read-only without the permission. useBranches now returns [] for a non-array payload instead of throwing "branches.map is not a function" and taking the page down with it. BookingLocationsScanTest built a Clinic around a Doctor loaded from a different manager, which Doctrine treats as a new entity; it flushed fine most runs and failed on cascade in others. It now loads the doctor from the same manager. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.6 KiB
9.6 KiB
چکلیست — تسک ۰۳ (تقویم منبع، استثنا، تعطیلات ملی)
وضعیت کلی: ✅ تکمیلشده (۲ ردیف 🔄 بازبینی چشمی · ۲ ردیف ⏳ با مقصد) · آخرین بازبینی: ۱۴۰۵/۰۵/۰۸
قواعد: _shared/definition-of-done.md · red-lines.md · 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 . |
✅ | |
| ۸.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | شیفت فصلی (۴.۲) · تاریخ شمسی در فرم (۵.۳) · اتصال تعطیلات به مسیر اسلاتی (۰.۴) |