The last structural gap from task 05 was the third occupancy mode. It is passive: the resource is genuinely held — nobody else can take that room while the patient waits for the anaesthetic — but the time is not work done. It blocks exactly like exclusive; the difference is in the report, where without it a room that spends half its day waiting reads as fully utilised. The mode is validated, offered in the segment editor and carried through to the plan. Everything else that was still marked as a deviation is now recorded in docs/architecture/deviations.md, one row each, in the form "what the plan said / what was built / why". That includes the ones I would defend (five plan services collapsed into one builder that only build() calls; a Skill foreign key instead of a JSON array, because a deleted skill in JSON fails silently) and the ones that are simply facts about the product (service_option does not exist here, so a column for it would sit empty until someone read it as a bug). The i18n section says plainly that the product is single-language and describes the order to migrate in if that changes — a translation layer with one language is an indirection, not an abstraction. All sixteen checklists now read zero pending and zero unresolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.9 KiB
9.9 KiB
چکلیست — تسک ۱۴ (رویدادهای دامنه و گزارش بهرهوری)
وضعیت کلی: ✅ تمامشده با انحرافهای ثبتشده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | --group=slot-mode-frozen سبز |
✅ | |
| ۰.۲ | AppointmentEvent دستنخورده |
✅ | جدول تفاوت در domain-events.md |
| ۰.۳ | پیامکهای موجود نشکستند | ✅ | Sms domain دست نخورد؛ تستهایش سبز |
| ۰.۴ | گزارش با داده حدسی ساخته نشد | ✅ | ⭐ مبنای «واقعی» فاصلهٔ ثبتشدهٔ اسلات است و همین در سند نوشته شد — نه حدسِ ساعت ورود و خروج |
۱. بکاند — رویدادها
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | DomainEvents + DomainEventPublisher + DomainEventLog |
✅ | تصمیم ثبتشده در deviations.md — بهجای کلاس پایهٔ DomainEvent و زیرکلاس per رویداد، یک فهرست بستهٔ نام + یک entity. چهارده زیرکلاس خالی فقط برای اینکه نام را در تایپ نگه دارند، همان کاری را میکنند که const میکند |
| ۱.۲ | payload فقط uuid و اسکالر | ✅ | ⭐ مقادیر غیراسکالر حذف میشوند، نه سریال |
| ۱.۳ | هر رویداد محیط دارد | ✅ | TenantOwnedTrait |
| ۱.۴ | outbox — record() فقط persist |
✅ | ⭐ تست rollback |
| ۱.۵ | worker انتشار | ✅ | منطق از Command به OutboxPublisher رفت و PublishDomainEventsMessage هر دقیقه در src/Schedule.php صادر میشود؛ همان worker-scheduler موجود مصرفش میکند |
| ۱.۶ | سقف تلاش، بدون حذف ردیف شکستخورده | ✅ | تست دارد |
| ۱.۷ | همهٔ نقاط به record() وصل شدند |
✅ | دوازده نقطه؛ چهار موردِ باقیمانده هم بسته شد |
| ۱.۸ | چهارده رویداد بند ۱۶ | ✅ | ⭐ هر چهارده تا نقطهٔ ثبت دارند. AppointmentCompleted بعد از ذخیرهٔ موفق (انتقال ردشده رویداد نمیگذارد) · AppointmentRescheduled رویداد سوم است نه جایگزین Booked/Cancelled |
| ۱.۹ | idempotency در مصرفکننده | ✅ | DomainEventMessage::$uuid + توضیح صریح در docblock و سند |
| ۱.۱۰ | worker با loop-wrap برای Coolify | ✅ | worker-scheduler از قبل loop-wrap دارد؛ سرویس تازه لازم نشد و همین در deploy/coolify.md نوشته شد |
| ۱.۱۱ | app:events:prune |
✅ | فقط ردیف منتشرشده حذف میشود؛ منتشرنشده مدرکِ گمشدن است |
| ۱.۱۲ | GET /domain-events فقط ادمین |
✅ | تست ۴۰۳/۲۰۰ |
۲. بکاند — گزارشها
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | ResourceUtilizationReporter |
✅ | |
| ۲.۲ | available × capacity |
✅ | ⭐ اتاق سهتخته سه برابر عرضه دارد |
| ۲.۳ | passive در occupied هست، در active نه |
✅ | active از appointment_segments.patient_present میآید |
| ۲.۴ | setup/cleanup در occupied |
✅ | از resource_occupancy که همه را دارد |
| ۲.۵ | released شمرده نمیشود |
✅ | BLOCKING_STATUSES |
| ۲.۶ | available = 0 → utilization = null |
✅ | ⭐ تست دارد |
| ۲.۷ | مرز بازه | ✅ | همپوشانی بازهای (start < to AND end > from) — دقیقتر از مرز روی یک سر |
| ۲.۸ | کوئری تجمعی بدون پیمایش | ✅ | ⭐ rawAvailabilityForAll تقویم همهٔ منابع را دستهای میخواند؛ تعطیلات/ساعت شعبه بیرون حلقه |
| ۲.۹ | تأیید وجود دادهٔ واقعی پیش از پیادهسازی | ✅ | ⭐ patient_sessions زمان شروع/پایان مراجعه ندارد، پس مبنای «واقعی» فاصلهٔ اسلات شد و همین در سند نوشته شد |
| ۲.۱۰ | آستانههای شدت | ✅ | ۳۰/۱۵/۵ درصد |
| ۲.۱۱ | انحراف منفی هم high |
✅ | ⭐ قدر مطلق |
| ۲.۱۲ | حداقل نمونه ۱۰ | ✅ | آستانه ۱۰ شد؛ ردیف زیر آستانه حذف نمیشود بلکه severity: null و below_min_sample میگیرد. یادداشت قبلی: ۳ انتخاب شد. با ۱۰، کلینیک کوچک در بازهٔ ۳۰ روزه گزارشی نمیبیند و ابزار تشخیص عملاً خاموش میماند؛ ۳ کمترین عددی است که میانگین معنا دارد |
| ۲.۱۳ | بازه > ۹۰ روز → ۴۲۲ | ✅ | |
| ۲.۱۴ | سه endpoint | ✅ |
۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | domain_events با سه ایندکس |
✅ | Version20260731084058 |
| ۳.۲ | ایندکس worker | ✅ | |
| ۳.۳ | هیچ جدول دیگری تغییر نکرد | ✅ | |
| ۳.۴ | TenantSchemaCoverageTest سبز |
✅ |
۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | ResourceUtilizationPage |
✅ | نمودار میلهای Recharts با رنگ از توکنها؛ منبعِ بیتقویم در نمودار نمیآید. یادداشت قبلی: جدول کامل است؛ نمودار Recharts اضافه نشد — با شش ستون عددی، جدول خواناتر از نمودار است |
| ۴.۲ | PlanAccuracyPage |
✅ | |
| ۴.۳ | نشان «ظرفیت هدررفته» | ✅ | زیر ۰٫۳ |
| ۴.۴ | توضیح active_ratio در خود UI |
✅ | ⭐ هم زیرنویس صفحه هم title ستون |
| ۴.۵ | utilization = null → — با توضیح |
✅ | بهجای عدد، لینک «تنظیم تقویم» — کار بعدی همان است |
| ۴.۶ | لینک اصلاح از PlanAccuracyPage |
✅ | ⭐ «ویرایش بخشهای این خدمت» |
| ۴.۷ | بازه با PersianDatePicker |
✅ | بازهٔ آماده و حالت دلخواه با PersianDateInput. یادداشت قبلی: برای گزارشی که همیشه «تا امروز» است سادهتر و کمخطاتر |
| ۴.۸ | وضعیت در URL | ✅ | useUrlState روی هر دو گزارش |
| ۴.۹ | DataTable با skeleton و empty state |
✅ | |
| ۴.۱۰ | رنگ نمودار از توکنها | ✅ | var(--primary) و var(--danger) — hex در دارکمود میشکست |
| ۴.۱۱ | هیچ رنگ hard-code | ✅ | |
| ۴.۱۲ | دارکمود | ✅ | با اسکرینشات واقعی دیده شد (CDP + seed کردن clinicpro-ui) |
| ۴.۱۳ | حالت فشرده | ✅ | همان اجرا با density: compact |
| ۴.۱۴ | RTL و موبایل | ✅ | جدولها اسکرول افقی داخلی دارند |
| ۴.۱۵ | رشتهها فارسی | ✅ | |
| ۴.۱۶ | backTo |
✅ |
۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | صندوق خروجی — rollback، انتشار، شکست، سقف تلاش | ✅ | ⭐ |
| ۵.۲ | payload فقط اسکالر | ✅ | مقادیر تودرتو و object حذف میشوند |
| ۵.۳ | بهرهوری — سنجهها | ✅ | ⭐ اشغال شامل انتظار، «کار مفید» نه — با نوبت و بخشهای واقعی |
| ۵.۴ | دقت برنامه — انحراف دوطرفه و نمونهٔ کم | ✅ | ⭐ |
| ۵.۵ | دسترسی و بازه | ✅ | ۴۲۲ بازه، ۴۰۳ رویدادها، جداسازی محیط |
| ۵.۶ | تعداد کوئری مستقل از تعداد منبع | ✅ | رشدِ خطی رد میشود؛ عددِ دقیق پین نمیشود |
اجرا: ddev exec php bin/phpunit tests/Report → ۱۶ تست.
۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | docs/api/reports.md |
✅ | معنی هر عدد + جدول شدت |
| ۶.۲ | docs/architecture/domain-events.md |
✅ | قرارداد، فهرست، outbox، idempotency، وضعیت انتشار هر رویداد |
| ۶.۳ | جدول تفاوت AppointmentEvent و DomainEventLog |
✅ | ⭐ |
۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ ⏳ بیدلیل نمانده | ✅ | همه با دلیل |
| ۷.۲ | bin/phpunit کامل سبز |
✅ | ۱۳۴۰ تست؛ flakeِ ثبتشده در تسک ۱۳ پیدا و رفع شد |
| ۷.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۷.۴ | phpstan بدون خطای جدید |
✅ | ۱۴ = baseline |
| ۷.۵ | npx tsc --noEmit و تستهای فرانت سبز |
✅ | ۶۳۴ تست |
| ۷.۶ | تستهای tenant سبز | ✅ | |
| ۷.۷ | docs/api/* بهروز |
✅ | |
| ۷.۸ | چکلیست UI کامل | ✅ | همه |
| ۷.۹ | پیامکهای موجود سرتاسر تست شدند | ✅ | مسیر Sms تغییر نکرد؛ رویدادها مسیر جدا دارند |
| ۷.۱۰ | دو کلاینت دیگر بررسی شدند | ✅ | با graphify بررسی شدند: هیچکدام گزارشها یا رویدادها را مصرف نمیکنند |
| ۷.۱۱ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۷.۱۲ | موارد بهتعویق با دلیل | ✅ | نمودار و URL-state (۴.۱/۴.۸) · تست کوئریشماری (۵.۶) |