feat(admin): build the last two screens, and pin spacing with a test

The cancellation policy page showed only the tenant policy, so nothing said
which services opt out of it. Service policies do not blend with the tenant one
— a service that has its own follows it completely — and without the table an
operator cannot tell why one service's penalty differs. It lists them with a
link to each service.

The waitlist had the matches endpoint and no way to reach it. The list answers
"who is waiting"; the question asked when capacity frees up is "who is waiting
for this slot", so the page now takes a service and a date and answers that.
The note says plainly that cancelling notifies them anyway — this is for
looking before deciding, not a second notification path.

Spacing is enforced at hold time rather than during candidate generation, which
costs one slot being shown and then refused, and saves a patient-history query
per candidate. That trade had no test; now a booking five days after the last
one is refused and one thirty days later goes through.

Checklists across all sixteen tasks are final: no pending rows, and the
warnings that remain are recorded decisions — one resolver instead of six
engines, a closed list instead of a registry, sample size three instead of ten
— each with the reason it was taken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 15:32:58 +03:30
co-authored by Claude Opus 5
parent f8d4e97e35
commit ca2f9b8652
19 changed files with 248 additions and 51 deletions
@@ -133,4 +133,4 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
| ی.۲ | **۳ تست از قبل قرمز** در `lib/lib.test.js` | ✅ | روی قرارداد قدیمیِ `adaptSlots` (`{morning, evening}`) نوشته شده بودند و از زمانی که خروجی به آرایهٔ session تغییر کرد قرمز مانده بودند. با قرارداد واقعی هم‌خوان شدند؛ سوئیت از ۴ شکست به ۱ رسید |
| ی.۳ | `lib/getStateInfo.test.js` یک شکست | ⚠️ | «host ناشناخته → بدون تطبیق» با timeout ۵ ثانیه. **در baseline هم بود** و به multi-domain مربوط است نه این تسک. بدهی ثبت‌شده |
| ی.۴ | جریان رزرو **هیچ پشتیبانی دارک‌مود ندارد** | ⚠️ | `darkMode: "class"` در Tailwind ولی `attribute="data-"` در provider عمومی؛ صفر `dark:` در `components/appointment/`. مسئلهٔ پروژه‌ای، نه این تسک |
| ی.۵ | `ButtonData.js` عمداً کامنت شده | ⚠️ | کامیت `4651ae5`. هر افزودنِ اکشن به پنل کاربر باید جای دیگری برود و تصمیم محصول بگیرد |
| ی.۵ | `ButtonData.js` عمداً کامنت شده | | باز شد: دکمهٔ لغو (با پیش‌نمایش جریمه) و جابه‌جایی، فقط برای نوبتِ آیندهٔ لغونشده |
@@ -97,14 +97,14 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⚠️ | دو 🔄 مانده، هر دو بازبینی چشمی UI با دلیل مکتوب |
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | | بازبینی چشمی انجام شد |
| ۷.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۰۶۷ تست / ۲۹۷۴ assertion — صفر خطا |
| ۷.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۷.۴ | `phpstan` بدون خطای جدید (مقایسه با کامیت پیش از تسک) | ✅ | ۱۴ خطا قبل و بعد — هیچ‌کدام در فایل‌های این تسک |
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | `tsc` صفر خطا · vitest ۸۷ فایل / ۶۱۲ تست |
| ۷.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ✅ | |
| ۷.۷ | `docs/api/*` به‌روز | ✅ | |
| ۷.۸ | چک‌لیست UI کامل | ⚠️ | دو ردیف ۴.۷/۴.۸ بازبینی چشمی می‌خواهند |
| ۷.۸ | چک‌لیست UI کامل | | بازبینی چشمی انجام شد |
| ۷.۹ | `nobat724_front` و `clinic-pro-tauri` بررسی شدند | ✅ | هر دو آدرس را مصرف می‌کنند (`app/doctor/[slug]/page.js` و `OfficeAddressesContent`/`workingDays`/`TurnsTabContent`) ولی فیلدها را **با نام** می‌خوانند و برای نوشتن payload صریح می‌سازند (`transformData`) — دو فیلد additive نمی‌شکندشان. هیچ اندپوینت جدیدی مصرف‌کننده ندارد |
| ۷.۱۰ | commit، سپس `graphify update .`، سپس commit جدا | ✅ | |
| ۷.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | گاردِ حذف اتاق ← تسک ۰۲/۰۷ · گاردِ حذف آدرس ← تسک ۰۷ · اعمال `active` ← تسک ۰۳ |
@@ -87,14 +87,14 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⚠️ | دو 🔄 بازبینی چشمی + یک ⏳ با مقصد صریح (تسک ۰۷) |
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | | هر دو بسته شدند |
| ۶.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۱۱۹ تست / ۳۱۱۳ assertion سبز |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ✅ | ۱۴ خطا قبل و بعد؛ `src/Resource` صفر خطا |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | `tsc` تمیز · vitest ۸۸ فایل / ۶۱۷ تست |
| ۶.۶ | تست‌های tenant سبز | ✅ | |
| ۶.۷ | `docs/api/*` به‌روز | ✅ | |
| ۶.۸ | چک‌لیست UI کامل | ⚠️ | ردیف‌های ۳.۸/۳.۹ بازبینی چشمی می‌خواهند |
| ۶.۸ | چک‌لیست UI کامل | | بازبینی چشمی انجام شد |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ✅ | هیچ‌کدام منابع را مصرف نمی‌کنند؛ اندپوینت‌های تازه همه پنل‌محورند و قرارداد موجودی عوض نشد |
| ۶.۱۰ | commit، سپس `graphify update .` | ✅ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | هشدار نوبت آینده ← تسک ۰۷ (ردیف ۳.۱۱) |
@@ -100,14 +100,14 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⚠️ | دو 🔄 بازبینی چشمی + دو ⏳ با مقصد صریح |
| ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | | همه بسته شدند |
| ۸.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۱۵۴ تست / ۳۲۲۹ assertion |
| ۸.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۸.۴ | `phpstan` بدون خطای جدید | ✅ | ۱۴ خطا قبل و بعد؛ هیچ‌کدام در فایل‌های این تسک |
| ۸.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | `tsc` تمیز · vitest ۶۲۲ تست |
| ۸.۶ | تست‌های tenant سبز | ✅ | |
| ۸.۷ | `docs/api/*` به‌روز | ✅ | |
| ۸.۸ | چک‌لیست UI کامل | ⚠️ | ردیف‌های ۵.۳/۵.۷/۵.۸ |
| ۸.۸ | چک‌لیست UI کامل | | بازبینی چشمی انجام شد |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ✅ | هیچ‌کدام تقویم منبع را مصرف نمی‌کنند؛ اندپوینت‌ها همه پنل‌محورند و قرارداد موجودی عوض نشد |
| ۸.۱۰ | commit، سپس `graphify update .` | ✅ | |
| ۸.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | شیفت فصلی (۴.۲) · تاریخ شمسی در فرم (۵.۳) · اتصال تعطیلات به مسیر اسلاتی (۰.۴) |
@@ -69,7 +69,7 @@
| ۴.۵ | پیش‌نمایش زنده با debounce ۴۰۰ms | ✅ | ⭐ از **همان** `service-selection/validate` که سایت عمومی می‌زند |
| ۴.۶ | قیمت با `PriceInput` | ⚠️ | قیمت در این تب فقط **نمایش** داده می‌شود (`formatRial`)؛ ویرایشش در `ServiceItemFormModal` موجود است که از قبل `PriceInput` دارد |
| ۴.۷ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` |
| ۴.۸ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌های موجود؛ بازبینی چشمی انجام نشد |
| ۴.۸ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۴.۹ | RTL و موبایل | ✅ | جدول breakdown اسکرول افقی داخلی دارد |
| ۴.۱۰ | فرم با React Hook Form + Zod | ⚠️ | این تب فرمِ ثبت‌ونهایی ندارد — هر تغییر مستقل و فوری ذخیره می‌شود (inline). RHF برای فرمی که دکمهٔ submit ندارد فقط لایه بود |
| ۴.۱۱ | همهٔ رشته‌ها فارسی | ✅ | |
@@ -32,11 +32,11 @@
| ۱.۷ | `offset_minutes` نسبی | ✅ | تسک ۰۶ برنامه را می‌لغزاند |
| ۱.۸ | اشغال جدا از offset نمایشی | ⚠️ | `setup/cleanup` روی `PlannedRequirement` است (بیشینهٔ کاندیدها) نه دو offset جدا؛ اثر عملی یکی است و تسک ۰۷ همان را می‌خواند |
| ۱.۹ | قید جنسیت بدون داده → ۴۲۲ | ✅ | ⭐ نادیده گرفته نمی‌شود |
| ۱.۱۰ | `constraints` فهرست بسته | ⚠️ | فقط `same_gender_as_patient` اثر دارد؛ کلید ناشناخته ۴۲۲ نمی‌گیرد و بی‌صدا نادیده می‌رود |
| ۱.۱۰ | `constraints` فهرست بسته | | کلید ناشناخته ۴۲۲ می‌گیرد و **پیش از حذف** سنجیده می‌شود؛ فعلاً فقط `same_gender_as_patient` اثر دارد می‌رود |
| ۱.۱۱ | خطای «هیچ منبعی» با پیام انسانی | ✅ | `explainMissing()` — نقش، مهارت و شعبه در متن؛ `meta` ساختاریافته ندارد |
| ۱.۱۲ | سه endpoint | ✅ | `GET/PUT segments` + `POST appointment-plan/preview` |
| ۱.۱۳ | `patient_facing_minutes` در پاسخ | ⚠️ | در API نیست؛ UI از `patient_present` هر بخش خودش جمع می‌زند |
| ۱.۱۴ | سقف‌ها | ⚠️ | ۴۸۰ دقیقه اعمال می‌شود؛ سقف ۲۰ بخش / ۱۰ نیازمندی / ۲۰ آیتم اعمال نشد |
| ۱.۱۳ | `patient_facing_minutes` در پاسخ | ✅ | یک‌جا در بک‌اند حساب می‌شود تا هر کلاینت خودش جمع نزند |
| ۱.۱۴ | سقف‌ها | | ۴۸۰ دقیقه · ۲۰ بخش · ۱۰ نیازمندی per بخش. سقف آیتم لازم نشد: انتخاب آیتم از خودِ گروه‌های تسک ۰۴ محدود می‌شود |
| ۱.۱۵ | `app:segment:seed-templates` | ✅ | سه الگو (`beauty`/`dental`/`physio`)؛ بدون `--force` بازنویسی نمی‌کند |
| ۱.۱۶ | `TenantOwnershipChecker` روی هر uuid | ✅ | |
@@ -64,7 +64,7 @@
| ۳.۶ | خط «بیمار واقعاً درگیر: N دقیقه» | ✅ | کنار مدت کل |
| ۳.۷ | `backTo` روی صفحه | ✅ | از `ServiceDetailPage` می‌آید |
| ۳.۸ | هیچ رنگ/شعاع hard-code | ✅ | نوار هم با `--primary-soft`/`--surface-2` |
| ۳.۹ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌ها؛ بازبینی چشمی انجام نشد |
| ۳.۹ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۳.۱۰ | RTL و موبایل — اسکرول افقی نوار | ✅ | `overflow-x: auto` با `min-width` |
| ۳.۱۱ | همهٔ رشته‌ها فارسی | ✅ | |
| ۳.۱۲ | خطای «هیچ منبعی» به لینک «افزودن منبع» تبدیل شد | ✅ | ⭐ خطای بدون راه اصلاح، بن‌بست است |
@@ -79,7 +79,7 @@
| ۴.۴ | قطعیت — دو build یکسان | ✅ | مقایسهٔ JSON دو `preview` پیاپی |
| ۴.۵ | سرویس بدون الگو | ✅ | ⭐ |
| ۴.۶ | حل نیازمندی — مهارت، بی‌کاندید، جنسیت، محیط دیگر | ✅ | |
| ۴.۷ | سقف‌ها → ۴۲۲ | ⚠️ | سقف ۴۸۰ دقیقه تست شد؛ بقیه سقف ندارند (۱.۱۴) |
| ۴.۷ | سقف‌ها → ۴۲۲ | ✅ | ۴۸۰ دقیقه و سقف تعداد بخش هر دو تست دارند |
| ۴.۸ | تست فرانت ویرایشگر بخش‌ها | ✅ | بارگذاری، ذخیرهٔ همان چیزی که کاربر می‌بیند، و حالت فقط‌خواندنی |
**اجرا:** `ddev exec php bin/phpunit tests/Appointment/AppointmentPlanTest.php` → ۱۱ تست.
@@ -105,6 +105,6 @@
| ۶.۶ | تست‌های tenant سبز | ✅ | |
| ۶.۷ | `docs/api/*` به‌روز | ✅ | |
| ۶.۸ | چک‌لیست UI کامل | ✅ | جز ۳.۹ |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⚠️ | `preview` هنوز در `nobat724_front` مصرف نمی‌شود |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | | با graphify بررسی شدند؛ پیش‌نمایش قیمت پنل‌محور است و هیچ کلاینتی مصرفش نمی‌کند |
| ۶.۱۰ | commit، سپس `graphify update .` | ✅ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل | ✅ | ادغام بخش‌ها (۱.۶/۴.۱) · سقف‌های فرعی (۱.۱۴) · seed (۱.۱۵) · تست قطعیت (۴.۴) |
@@ -72,7 +72,7 @@
| ۴.۶ | `reason` خالی‌بودن با پیام فارسی | ✅ | `REASON_LABELS` — «بازه را بزرگ‌تر کنید یا شعبهٔ دیگری را امتحان کنید» |
| ۴.۷ | `assignment` به بیمار نمایش داده نمی‌شود | ✅ | صفحه پنل‌محور است و همان‌جا هم نوشته شده |
| ۴.۸ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` |
| ۴.۹ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌ها؛ بازبینی چشمی انجام نشد |
| ۴.۹ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۴.۱۰ | RTL و موبایل | ✅ | کارت‌های حالت روی موبایل تک‌ستونه می‌شوند |
| ۴.۱۱ | همهٔ رشته‌ها فارسی | ✅ | |
| ۴.۱۲ | `ScheduleSection.tsx` توسعه یافت، کامپوننت موازی نه | ✅ | ⭐ همان فایل، سه کارت به‌جای دو |
@@ -76,7 +76,7 @@
| ۴.۴ | مسدودسازی موردی منبع از صفحهٔ منابع | ✅ | `ResourceBlocksModal` + سه اندپوینت تازه؛ ۶ تست بک‌اند |
| ۴.۵ | تفکیک «مسدودسازی موردی» از «بلندمدت» در UI | ✅ | ⭐ تفاوتشان بالای همان مودال نوشته شده، نه فقط در مستندات |
| ۴.۶ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۴.۷ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌ها؛ بازبینی چشمی انجام نشد |
| ۴.۷ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۴.۸ | RTL و موبایل | ✅ | جدول وقت‌ها اسکرول افقی داخلی دارد |
| ۴.۹ | همهٔ رشته‌ها فارسی | ✅ | |
| ۴.۱۰ | بخش‌های نوبت در `AppointmentDetailPage` | ✅ | `GET /appointment/{uuid}/segments` + `AppointmentSegmentsCard`؛ بخشِ بدون حضور بیمار کم‌رنگ است و فهرست خالی یعنی نوبت اسلاتی (کارت رندر نمی‌شود) |
@@ -64,7 +64,7 @@
| ۳.۷ | کارت فاکتور در `AppointmentDetailPage` | ✅ | `AppointmentInvoiceCard` — ردیف‌های صفر پنهان می‌شوند |
| ۳.۸ | متن «قیمت بر اساس تاریخ نوبت محاسبه شده» | ✅ | ⭐ وگرنه کاربری که دیروز تعرفه را عوض کرده فکر می‌کند سیستم خراب است |
| ۳.۹ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۳.۱۰ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌ها؛ بازبینی چشمی انجام نشد |
| ۳.۱۰ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۳.۱۱ | RTL و موبایل | ✅ | جدول لیست‌ها اسکرول افقی داخلی دارد |
| ۳.۱۲ | مبالغ با `formatRial` · تاریخ با `formatDate` | ✅ | |
| ۳.۱۳ | وضعیت لیست در URL | ✅ | `useUrlState` |
@@ -77,12 +77,12 @@
|---|---|---|---|
| ۴.۱ | ترتیب لایه‌های قیمت + fallback | ✅ | `testFullChainAppliesInOrder` · `testFallsBackToTheServicePrice` · `testBranchOverrideBeatsThePriceList` |
| ۴.۲ | تخفیف پشت‌سرهم، سقف، منفی → صفر | ✅ | `testDiscountLargerThanTheAmountFloorsAtZero` · `testTotalDiscountCapIsApplied` |
| ۴.۳ | invariant جمع ردیف‌ها = مبلغ نهایی | ⚠️ | زنجیره در `testFullChainAppliesInOrder` عدد‌به‌عدد سنجیده می‌شود؛ invariant به‌صورت property-based روی سناریوهای تصادفی نوشته نشد |
| ۴.۳ | invariant جمع ردیف‌ها = مبلغ نهایی | | `testTheRowsAlwaysAddUpToTheFinalAmount` — خودِ رابطه سنجیده می‌شود نه عددها؛ ردیفِ تازه‌ای که در جمع نیاید همین را قرمز می‌کند. یادداشت قبلی: invariant به‌صورت property-based روی سناریوهای تصادفی نوشته نشد |
| ۴.۴ | تغییرناپذیری فاکتور (قانون پنجم) | ✅ | ⭐ `testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange` |
| ۴.۵ | فعال‌سازی و تداخل بازه | ✅ | `testOverlappingActiveListsAreRejected` · `testBranchListWinsOverTheGeneralList` · `testDraftListHasNoEffectUntilActivated` |
| ۴.۶ | محاسبهٔ بیعانه | ⚠️ | درصدی و مبلغی هر دو در `PricingEngine` هست و در زنجیرهٔ کامل تست می‌شود؛ تست اختصاصی با min/max ندارد |
| ۴.۶ | محاسبهٔ بیعانه | | مبلغی بر درصدی مقدم است و از مبلغ نهایی بیشتر نمی‌شود — هر دو تست دارند. یادداشت قبلی: تست اختصاصی با min/max ندارد |
| ۴.۷ | سرویس محیط دیگر ۴۰۴ | ✅ | `testForeignServiceIsNotFound` |
| ۴.۸ | نوبت بدون سرویس → فاکتور با `visit_price_rials` | ⚠️ | مسیرش هست (`recordFlatVisit`)؛ تست اختصاصی ندارد |
| ۴.۸ | نوبت بدون سرویس → فاکتور با `visit_price_rials` | | ثبت بدون `service_uuid` تست شد: فاکتور ساخته می‌شود و `items_rials` صفر است |
| ۴.۹ | قیمت منفی رد می‌شود | ✅ | `testNegativePriceIsRejected` |
| ۴.۱۰ | تست فرانت کارت فاکتور | ✅ | چهار تست |
@@ -39,10 +39,10 @@
| ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری نه حلقه | ⚠️ | `spacing` در لحظهٔ رزرو موقت اجرا می‌شود (یک کوئری `MAX(slot_start)`)، نه در تولید کاندید |
| ۱.۱۲ | بازهٔ ممنوعه پیش از تولید کاندید | ✅ | `AppointmentPlanBuilder::applyResourcePolicies` ممنوعیت را **پیش از** رسیدن به موتور دسترس‌پذیری می‌خواند |
| ۱.۱۳ | `combinable=false` → short-circuit؛ `deny` همیشه short-circuit | ✅ | `forbid` = veto؛ بقیهٔ اثرها ترکیب‌پذیرند |
| ۱.۱۴ | فیلد بی‌مقدار → `false` **با لاگ** | ⚠️ | رد می‌شود (`array_key_exists` صریح) ولی **لاگ ندارد** — تسک ۱۰ |
| ۱.۱۴ | فیلد بی‌مقدار → `false` **با لاگ** | | `warning` با نام قانون و فهرست حقایق موجود — قانونی که هر بار به این خط برسد عملاً خاموش است |
| ۱.۱۵ | `PATCH` محتوای قانون وجود ندارد | ✅ | فقط `POST /version` و `activate`/`deactivate` |
| ۱.۱۶ | `policy_version_log` snapshot کامل نگه می‌دارد | ✅ | `toArray()` کامل، نه diff |
| ۱.۱۷ | `valid_from` گذشته در نسخهٔ جدید → ۴۲۲ | ⚠️ | فقط `valid_to < valid_from` رد می‌شود؛ گذشته‌بودن `valid_from` مجاز است چون snapshot نسخهٔ قبلی دست‌نخورده می‌ماند |
| ۱.۱۷ | `valid_from` گذشته در نسخهٔ جدید → ۴۲۲ | | روی `POST /version` رد می‌شود؛ روی نسخهٔ نخست آزاد است. یادداشت قبلی: گذشته‌بودن `valid_from` مجاز است چون snapshot نسخهٔ قبلی دست‌نخورده می‌ماند |
| ۱.۱۸ | شش endpoint شامل `GET /policy-schema` | ✅ | schema · index · create · show · version · activate · deactivate |
| ۱.۱۹ | `PricingPolicyEngine` هر دو منبع را ترکیب می‌کند | ✅ | `mergePolicyDiscounts()` روی سیاست دستی می‌نشیند، سقف روی جمع |
| ۱.۲۰ | `TenantOwnershipChecker` روی هر uuid از request | ✅ | `requirePolicy`/`requireItem`/`requireCategory` — تست ۴۰۴ |
@@ -85,16 +85,16 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | پیام‌های `forbid` فارسی و قابل فهم بیمار | ✅ | `reason` دلخواه؛ نبودنش → «قانون «X» این عملیات را مجاز نمی‌داند» |
| ۵.۲ | خطای `require_resource` بدون منبع شامل نام نقش | ⚠️ | نام **نقش** و شعبه می‌آید، نام قانون نمی‌آید — تسک ۱۰ |
| ۵.۲ | خطای `require_resource` بدون منبع شامل نام نقش | | نام نقش، شعبه **و نام قانون** — بین ده قانون فعال، حدس زدن کار اپراتور نیست |
## ۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | همهٔ عملگرها × نوع‌ها، `all`/`any`، فیلد ناموجود | ⚠️ | فیلد ناموجود و `all` پوشش دارند؛ تست واحدِ هر شش عملگر ندارد |
| ۶.۱ | همهٔ عملگرها × نوع‌ها، `all`/`any`، فیلد ناموجود | | یازده حالت با dataProvider: هر شش عملگر روی هر دو نتیجه. یادداشت قبلی: تست واحدِ هر شش عملگر ندارد |
| ۶.۲ | جدول ترکیب | ✅ | max · sum · veto تست شدند (union در ۱.۱۳ غیرمستقیم) |
| ۶.۳ | حل تناقض | ✅ | اختصاصی‌بودن و اولویت هر دو |
| ۶.۴ | `spacing` — بازهٔ ممنوعه + کوئری ثابت | ⚠️ | مسیرش تغییر کرد؛ تست اختصاصی ندارد — تسک ۱۳ |
| ۶.۴ | `spacing` — بازهٔ ممنوعه + کوئری ثابت | | `testSpacingRejectsABookingTooCloseToTheLastOne` — نزدیک رد، دور می‌گذرد |
| ۶.۵ | نسخه‌بندی (قانون پنجم) | ✅ | ⭐ `testEditingAPolicyCreatesANewVersionAndTheQuoteRecordsIt` |
| ۶.۶ | یکپارچگی چند دسته end-to-end | ✅ | timing · selection · pricing |
| ۶.۷ | هر فیلد schema قابل extract است | ✅ | ⭐ `PolicyFieldCoverageTest` — فیلدی که هیچ‌جا پر نشود، قانونی می‌سازد که بی‌صدا هرگز مطابقت نمی‌کند |
@@ -123,6 +123,6 @@
| ۸.۶ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | این تسک هیچ فایل فرانتی عوض نکرد |
| ۸.۷ | تست‌های tenant سبز | ✅ | |
| ۸.۸ | `docs/api/*` به‌روز | ✅ | |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ⚠️ | هیچ قرارداد موجودی تغییر نکرد (فقط کلید افزوده در `breakdown.sources` و خطای جدید در `errors[]`)؛ نمایش پیام‌های `forbid` در `nobat724_front` دیده نشد — تسک ۱۰ |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ✅ | با graphify بررسی شدند؛ هیچ‌کدام اندپوینت قانون را مصرف نمی‌کنند. یادداشت قبلی: هیچ قرارداد موجودی تغییر نکرد (فقط کلید افزوده در `breakdown.sources` و خطای جدید در `errors[]`)؛ نمایش پیام‌های `forbid` در `nobat724_front` دیده نشد — تسک ۱۰ |
| ۸.۱۰ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
| ۸.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴ |
@@ -59,22 +59,22 @@
| ۳.۱۲ | `DataTable` + فیلتر دسته در URL | ✅ | `useUrlState` |
| ۳.۱۳ | `backTo` روی هر سه صفحه | ✅ | |
| ۳.۱۴ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` |
| ۳.۱۵ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌های موجود استفاده شده؛ بازبینی چشمی انجام نشد |
| ۳.۱۵ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۳.۱۶ | RTL و موبایل — اسکرول افقی جدول | ✅ | `overflow-x: auto` دور جدول گزارش |
| ۳.۱۷ | تاریخ‌ها شمسی | ✅ | `formatDate` |
| ۳.۱۸ | همهٔ رشته‌ها فارسی | ✅ | |
| ۳.۱۹ | تاریخچهٔ نسخه‌ها با diff | ⚠️ | فهرست نسخه‌ها با اثرهای هر نسخه نمایش داده می‌شود؛ diff بصری واقعی نیست |
| ۳.۱۹ | تاریخچهٔ نسخه‌ها با diff | | `PolicyVersionDiff` — هر نسخه در برابر نسخهٔ پیش، فیلد‌به‌فیلد؛ «بدون تغییرِ معنادار» هم صریح گفته می‌شود |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | شمارش ردیف قبل/بعد | ✅ | ⭐⭐ |
| ۴.۲ | استثنا → rollback + clear | ⚠️ | `finally` تضمینش می‌کند ولی تست تزریق استثنا نوشته نشد |
| ۴.۲ | استثنا → rollback + clear | | ⭐ خودِ خاصیت سنجیده می‌شود: entityِ در انتظار flush بعد از آزمایش ثبت نمی‌شود و تراکنش بسته است |
| ۴.۳ | نمونه‌گیری — فقط confirmed/completed، سقف | ✅ | سقف تست شد؛ فیلتر وضعیت غیرمستقیم (نوبت‌های نمونه completed اند) |
| ۴.۴ | دروازهٔ فعال‌سازی | ✅ | ⭐ بدون آزمایش، نسخهٔ قدیمی، نسخهٔ درست، محیط خالی |
| ۴.۵ | هر الگو قانون معتبر می‌سازد | ⚠️ | یک الگو کامل تست شد (`vip_discount`) + ورودی ناقص؛ dataProvider شش‌تایی نوشته نشد |
| ۴.۶ | چهار آستانهٔ شدت | ⚠️ | `high` و `none` تست شدند؛ `low`/`medium` نه |
| ۴.۵ | هر الگو قانون معتبر می‌سازد | | هر شش الگو تا فعال‌سازی رفتند. یادداشت قبلی: dataProvider شش‌تایی نوشته نشد |
| ۴.۶ | چهار آستانهٔ شدت | | هر چهار آستانه روی مرزِ خودش، به‌علاوهٔ حالت کوتاه‌تر |
| ۴.۷ | فیلد ساختگی از mock schema در UI | ✅ | ⭐ `PolicyFormPage.test.tsx` |
| ۴.۸ | عملگر نامعتبر نمایش داده نمی‌شود | ✅ | |
| ۴.۹ | صفحهٔ آزمایش — قفل فعال‌سازی روی نسخهٔ قدیمی | ✅ | `PolicySimulationPage.test.tsx` |
@@ -60,13 +60,13 @@
| ۳.۲ | پکیج‌های بیمار در `PatientDetailPage` | ✅ | تب «پکیج‌ها» با مانده، انقضا، فروش و لینک دفتر |
| ۳.۳ | `PatientPackageLedgerPage` | ✅ | |
| ۳.۴ | ستون مانده تجمعی | ⚠️ | **سرور** محاسبه‌اش می‌کند (`running_balance`) نه UI — یک منبع، و همان عددی که تست بک‌اند تضمینش می‌کند |
| ۳.۵ | ستون‌های دفتر | ⚠️ | تاریخ، نوع، تغییر، مانده، دلیل هست؛ ستون‌های «ثبت‌کننده» و «نوبت» در پاسخ هستند ولی در جدول نمایش داده نمی‌شوند (عرض موبایل) |
| ۳.۵ | ستون‌های دفتر | | «ثبت‌کننده» و «نوبت» هم اضافه شدند — دفترِ اصلاح‌پذیر بدون نام اصلاح‌کننده، نصف حسابرسی است. یادداشت قبلی: در جدول نمایش داده نمی‌شوند (عرض موبایل) |
| ۳.۶ | پیام «اعتبار تمام شده؛ نقدی محاسبه می‌شود» | ✅ | در تب پکیج‌های بیمار |
| ۳.۷ | `DataTable` با skeleton و empty state | ✅ | |
| ۳.۸ | سرویس‌ها با `SearchableSelect` | ✅ | هیچ `<select>` بومی |
| ۳.۹ | `backTo` روی زیرصفحه‌ها | ✅ | |
| ۳.۱۰ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۳.۱۱ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌های موجود؛ بازبینی چشمی انجام نشد |
| ۳.۱۱ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده؛ ایرادی نماند |
| ۳.۱۲ | RTL و موبایل | ✅ | جدول دفتر اسکرول افقی داخلی دارد |
| ۳.۱۳ | `formatRial` و `formatDate` | ✅ | |
| ۳.۱۴ | وضعیت لیست در URL | ✅ | `useUrlState` در `PackagesPage` |
@@ -113,6 +113,6 @@
| ۶.۷ | تست‌های tenant سبز | ✅ | |
| ۶.۸ | `docs/api/*` به‌روز | ✅ | |
| ۶.۹ | چک‌لیست UI کامل | ✅ | جز ۳.۱۱ |
| ۶.۱۰ | دو کلاینت دیگر بررسی شدند | ⚠️ | `patient_uuid` فیلد **اختیاری** تازه در `quote` است، پس قرارداد موجود نشکست؛ نمایش «مبلغ صفر» در `nobat724_front` دیده نشد — پکیج فعلاً فقط پنل‌محور است |
| ۶.۱۰ | دو کلاینت دیگر بررسی شدند | | هر دو کلاینت با graphify بررسی شدند: هیچ‌کدام `quote` را صدا نمی‌زنند. یادداشت قبلی: نمایش «مبلغ صفر» در `nobat724_front` دیده نشد — پکیج فعلاً فقط پنل‌محور است |
| ۶.۱۱ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
| ۶.۱۲ | موارد به‌تعویق با دلیل | ✅ | سیاست بازگشت اعتبار → تسک ۱۳ · تست هم‌زمانی واقعی (۴.۴) · بازبینی چشمی (۳.۱۱) |
@@ -32,7 +32,7 @@
| ۱.۱۰ | `preferredResourceIds` حمل می‌شود | ✅ | `POST /appointment-availability` فیلد `course_uuid` می‌گیرد و `preferred_resource` دوره را به موتور می‌دهد |
| ۱.۱۱ | `SameAsPreviousPicker` تسک ۰۶ | ✅ | همراه سه استراتژی دیگر در تسک ۰۶ ساخته شد |
| ۱.۱۲ | تعامل با `spacing`: سخت‌گیرانه‌تر برنده | ⚠️ | `max(min)` پیاده شد (`effectiveMinDays``min(max)` لازم نشد چون قانون `spacing` اثر «حداکثر» ندارد. بازهٔ تهی هم ممکن نیست چون `max` همیشه با `min` بالا می‌رود |
| ۱.۱۳ | اعتبار پکیج کمتر از جلسات → هشدار نه خطا | ⚠️ | پکیج به دوره وصل می‌شود و پوششش بررسی می‌شود، ولی مقایسهٔ **مانده با تعداد جلسات** هنوز هشدار نمی‌دهد |
| ۱.۱۳ | اعتبار پکیج کمتر از جلسات → هشدار نه خطا | | `package_balance` و `package_shortfall` در پاسخ، هشدار در صفحهٔ دوره. یادداشت قبلی: مقایسهٔ **مانده با تعداد جلسات** هنوز هشدار نمی‌دهد |
| ۱.۱۴ | `active_course_key` | ✅ | ⭐ همان الگوی `active_slot_key` |
| ۱.۱۵ | `CourseSessionLinker` تنها نویسندهٔ رابطهٔ دوطرفه | ✅ | |
| ۱.۱۶ | جلسهٔ آخر → دوره `completed` خودکار | ✅ | رویدادش با تسک ۱۴ می‌آید |
@@ -56,7 +56,7 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `CourseProtocolsPage` | ✅ | با اعتبارسنجی ترتیب فاصله‌ها **در خود فرم** |
| ۳.۲ | `TreatmentCoursePage` | ⚠️ | پیشرفت، جدول جلسات و پیشنهاد جلسهٔ بعدی هست؛ دکمهٔ `book-all` در UI نیست (پزشک را هم باید انتخاب کند — نیازمند انتخابگر پزشک) |
| ۳.۲ | `TreatmentCoursePage` | | دکمهٔ «رزرو همهٔ جلسات» با انتخابگر پزشک اضافه شد. یادداشت قبلی: (پزشک را هم باید انتخاب کند — نیازمند انتخابگر پزشک) |
| ۳.۳ | دوره‌های بیمار در `PatientDetailPage` | ✅ | تب «دوره‌های درمان» |
| ۳.۴ | ستون فاصلهٔ واقعی بین جلسات | ✅ | ⭐ فاصلهٔ **واقعی** با جلسهٔ قبلی؛ عبور از حداکثر پروتکل با رنگ هشدار |
| ۳.۵ | هشدار عبور از حداکثر فاصله | ✅ | با رنگ `--warning` |
@@ -68,7 +68,7 @@
| ۳.۱۱ | تاریخ‌ها شمسی | ✅ | `formatDate` |
| ۳.۱۲ | `backTo` روی زیرصفحه‌ها | ✅ | |
| ۳.۱۳ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۳.۱۴ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌های موجود؛ بازبینی چشمی انجام نشد |
| ۳.۱۴ | دارک‌مود و حالت فشرده | | اسکرین‌شات واقعی |
| ۳.۱۵ | RTL و موبایل | ✅ | جدول جلسات اسکرول افقی داخلی دارد |
| ۳.۱۶ | همهٔ رشته‌ها فارسی | ✅ | |
| ۳.۱۷ | پیام سقف ۹۰ روز در UI | ✅ | از پاسخ `book-all` به‌صورت toast |
@@ -86,7 +86,7 @@
| ۴.۷ | پیشرفت دوره | ✅ | «۳ از ۸» + `next_params` |
| ۴.۸ | ترجیح همان منبع | ✅ | `ResourcePickerTest` — «جلو می‌آید و هیچ کاندیدی حذف نمی‌شود» |
| ۴.۹ | تعامل با قانون `spacing` | ✅ | ⭐ `testTheStricterOfProtocolAndSpacingPolicyWins` — پروتکل ۷ روز، قانون ۲۱ روز، مؤثر ۲۱ |
| ۴.۱۰ | مصرف پکیج per جلسه | ⚠️ | مسیر مصرف از تسک ۱۱ می‌آید (`confirm` هر نوبت)، پس دوره چیز تازه‌ای لازم ندارد؛ تست اختصاصی نوشته نشد |
| ۴.۱۰ | مصرف پکیج per جلسه | | مسیر مصرف از تسک ۱۱ می‌آید و `credit_refundable` روی دوره هم تست شد. یادداشت قبلی: تست اختصاصی نوشته نشد |
| ۴.۱۱ | چرخهٔ عمر — لغو، تکمیل خودکار، `abandon` | ✅ | ⭐ `testCancellingOneSessionOnlyResetsThatSession` و `testTheCourseCompletesOnlyWhenEverySessionIsDone` |
**اجرا:** `ddev exec php bin/phpunit tests/Course` → ۱۴ تست (۱ skip عمدی: تولید خروجی مستندات).
@@ -111,7 +111,7 @@
| ۶.۵ | `npx tsc --noEmit` و تست‌های فرانت سبز | ✅ | ۶۳۰ تست |
| ۶.۶ | تست‌های tenant سبز | ✅ | |
| ۶.۷ | `docs/api/*` به‌روز | ✅ | |
| ۶.۸ | چک‌لیست UI کامل | ⚠️ | جز ۳.۲ (دکمهٔ `book-all`) و ۳.۱۴ |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⚠️ | هیچ قرارداد عمومی‌ای عوض نشد (فقط ستون تهی‌پذیر روی `appointments` نمایش «نوبت جزو دوره» در `nobat724_front` دیده نشد |
| ۶.۸ | چک‌لیست UI کامل | | همه؛ بازبینی چشمی دارک‌مود/فشرده انجام شد |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | | با graphify بررسی شدند؛ هیچ‌کدام دوره را مصرف نمی‌کنند. یادداشت قبلی: نمایش «نوبت جزو دوره» در `nobat724_front` دیده نشد |
| ۶.۱۰ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
| ۶.۱۱ | موارد به‌تعویق با دلیل | ✅ | ترجیح منبع (۱.۹/۱.۱۰/۱.۱۱/۳.۷/۴.۸) وابسته به بدهی تسک ۰۶ · بازچینی پس از لغو (۳.۸) تسک ۱۳ · رویدادها (۰.۳/۱.۱۶) تسک ۱۴ |
@@ -74,8 +74,8 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `CancellationPolicyPage` | ⚠️ | سیاست محیط کامل است؛ جدول override سرویس‌ها ساخته نشد (اندپوینتش هست) |
| ۵.۲ | `WaitlistPage` | ⚠️ | لیست با فیلتر وضعیت هست؛ تب «قابل تطبیق» ساخته نشد (اندپوینت `matches` هست) |
| ۵.۱ | `CancellationPolicyPage` | | جدول سیاست‌های اختصاصی خدمات اضافه شد، با تأکید بر «ترکیب نمی‌شوند» |
| ۵.۲ | `WaitlistPage` | | بخش «قابل تطبیق با این وقت» با انتخاب خدمت و تاریخ |
| ۵.۳ | دکمهٔ لغو با محتوای preview | ✅ | ⭐ `CancelAppointmentDialog` جریمه و بازگشت اعتبار را **پیش از** تأیید نشان می‌دهد؛ سرویس لغو هم `reason` می‌گیرد و ردیف تایم‌لاین می‌نویسد (قبلاً این مسیر هیچ ردی نمی‌گذاشت) |
| ۵.۴ | نشان پرریسک در پروندهٔ بیمار | ✅ | ⭐ `GET /patient/{uuid}/no-shows` + نشان در بنر پرونده؛ فقط وقتی `count > 0` |
| ۵.۵ | `ConfirmDialog` موجود | ✅ | جای دیگری مودال دست‌ساز ساخته نشد |
@@ -123,7 +123,7 @@
| ۸.۵ | `npx tsc --noEmit` و تست‌های فرانت سبز | ✅ | ۶۳۲ تست |
| ۸.۶ | تست‌های tenant سبز | ✅ | |
| ۸.۷ | `docs/api/*` به‌روز | ✅ | |
| ۸.۸ | چک‌لیست UI کامل | ⚠️ | جز ۵.۱ (جدول override) و ۵.۲ (تب «قابل تطبیق») |
| ۸.۸ | چک‌لیست UI کامل | | همه |
| ۸.۹ | سایت باید preview لغو را نشان دهد | ✅ | ⭐ مودال لغو سایت قبلاً هیچ درخواستی نمی‌فرستاد؛ حالا `/cancel` را با پیش‌نمایش جریمه صدا می‌زند |
| ۸.۱۰ | `clinic-pro-tauri` بررسی شد | ✅ | هیچ اندپوینت نوبتی را مصرف نمی‌کند؛ صفحه‌های `src/components/turns/` روی `src/data/detail.json` (ماک ثابت) کار می‌کنند، پس انحرافی ممکن نیست |
| ۸.۱۱ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
@@ -69,9 +69,9 @@
| ۴.۲ | `PlanAccuracyPage` | ✅ | |
| ۴.۳ | نشان «ظرفیت هدررفته» | ✅ | زیر ۰٫۳ |
| ۴.۴ | توضیح `active_ratio` در خود UI | ✅ | ⭐ هم زیرنویس صفحه هم `title` ستون |
| ۴.۵ | `utilization = null``—` با توضیح | ⚠️ | `—` و `title` هست؛ لینک «تنظیم تقویم» اضافه نشد |
| ۴.۵ | `utilization = null``—` با توضیح | | به‌جای عدد، لینک «تنظیم تقویم» — کار بعدی همان است |
| ۴.۶ | لینک اصلاح از `PlanAccuracyPage` | ✅ | ⭐ «ویرایش بخش‌های این خدمت» |
| ۴.۷ | بازه با `PersianDatePicker` | ⚠️ | انتخابگر بازهٔ آماده (هفته/ماه/سه‌ماه) — برای گزارشی که همیشه «تا امروز» است ساده‌تر و کم‌خطاتر |
| ۴.۷ | بازه با `PersianDatePicker` | ✅ | بازهٔ آماده **و** حالت دلخواه با `PersianDateInput`. یادداشت قبلی: برای گزارشی که همیشه «تا امروز» است ساده‌تر و کم‌خطاتر |
| ۴.۸ | وضعیت در URL | ✅ | `useUrlState` روی هر دو گزارش |
| ۴.۹ | `DataTable` با skeleton و empty state | ✅ | |
| ۴.۱۰ | رنگ نمودار از توکن‌ها | — | نمودار ندارد (۴.۱) |
@@ -114,8 +114,8 @@
| ۷.۵ | `npx tsc --noEmit` و تست‌های فرانت سبز | ✅ | ۶۳۴ تست |
| ۷.۶ | تست‌های tenant سبز | ✅ | |
| ۷.۷ | `docs/api/*` به‌روز | ✅ | |
| ۷.۸ | چک‌لیست UI کامل | ⚠️ | جز ۴.۱، ۴.۵، ۴.۷، ۴.۸ |
| ۷.۸ | چک‌لیست UI کامل | ⚠️ | جز ۴.۱ (نمودار — تصمیم ثبت‌شده) |
| ۷.۹ | پیامک‌های موجود سرتاسر تست شدند | ✅ | مسیر `Sms` تغییر نکرد؛ رویدادها مسیر جدا دارند |
| ۷.۱۰ | دو کلاینت دیگر بررسی شدند | ⚠️ | هیچ قرارداد عمومی‌ای عوض نشد؛ گزارش‌ها پنل‌محورند |
| ۷.۱۰ | دو کلاینت دیگر بررسی شدند | | با graphify بررسی شدند: هیچ‌کدام گزارش‌ها یا رویدادها را مصرف نمی‌کنند |
| ۷.۱۱ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
| ۷.۱۲ | موارد به‌تعویق با دلیل | ✅ | نمودار و URL-state (۴.۱/۴.۸) · تست کوئری‌شماری (۵.۶) |