feat(branch): admin UI for branch working hours and rooms, plus real API docs
Three pages, all on the existing design system: BranchesPage lists the current environment's booking locations with their working-hours and active-room counts, and two subpages edit the week and the rooms. The list page deliberately does not create or rename a branch — clinic and doctor detail pages already do that, and duplicating it would give one physical place two edit surfaces. Route permission reuses `appointment_settings` rather than inventing a new one. Two real bugs fell out of exercising this end to end: `days` was serialising as a JSON *array*, not an object keyed "0".."6" — keys 0..6 are sequential so json_encode collapses them to a list. The client reads days["0"] either way, so nothing looked broken, but the response shape was unstable: one missing day would flip the same field to an object. The controller now casts to stdClass and WorkingHoursTest::testDaysIsAJsonObjectNotAnArray pins it. Found by curling the endpoint for the docs, not by any test. `<input type="time">` caps at 23:59, so it can neither display nor produce the legal end value 1440. An all-day range would have vanished from the form and been corrupted by the first save. Ranges now carry an explicit end-of-day flag, with a round-trip test proving 1440 survives. docs/api/branch.md documents all eight endpoints with responses captured from real curl runs against ddev, including the 422 and 404 bodies. doctor.md records that active/timezone now appear on all nine existing address endpoints (additive), and tenancy.md gains the two lessons this task taught: an aggregate child whose root is itself declared global inherits no environment and needs a real pair, and TenantFilter is not a substitute for an explicit ownership check because hard isolation only applies to a *chosen* context. Verified: phpunit 1067 tests / 2974 assertions green; slot-mode frozen contract green; phpstan 14 errors before and after, none in touched files; tsc clean; vitest 87 files / 612 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# چکلیست — تسک ۰۱ (شعبه و اتاق)
|
||||
|
||||
**وضعیت کلی:** 🔄 در حال انجام · **آخرین بازبینی:** —
|
||||
**وضعیت کلی:** ✅ تکمیلشده (۲ ردیف 🔄 بازبینی چشمی) · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۸
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md) ·
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۰.۲ | `SlotCalculatorService` دستنخورده | ⏳ | این تسک به آن کاری ندارد |
|
||||
| ۰.۳ | `location_id` در JSON برنامهٔ هفتگی دستنخورده | ⏳ | شعبه = همان `doctor_addresses.id` |
|
||||
| ۰.۴ | `DoctorAddress` هیچ ستونی حذف/تغییر نداد | ⏳ | فقط `active` و `timezone` با `DEFAULT` |
|
||||
| ۰.۵ | `active=false` هیچ اثری بر محاسبهٔ اسلات ندارد | ⏳ | اعمالش تسک ۰۳ است |
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | `--group=slot-mode-frozen` — ۳ تست / ۸ assertion سبز |
|
||||
| ۰.۲ | `SlotCalculatorService` دستنخورده | ✅ | صفر تغییر در فایل |
|
||||
| ۰.۳ | `location_id` در JSON برنامهٔ هفتگی دستنخورده | ✅ | شعبه = همان `doctor_addresses.id`؛ JSON دستنخورده |
|
||||
| ۰.۴ | `DoctorAddress` هیچ ستونی حذف/تغییر نداد | ✅ | فقط دو ستون `NOT NULL DEFAULT` |
|
||||
| ۰.۵ | `active=false` هیچ اثری بر محاسبهٔ اسلات ندارد | ✅ | فقط ذخیره میشود؛ در `branch.md` نوشته شد |
|
||||
|
||||
## ۱. طرح
|
||||
|
||||
@@ -30,81 +30,81 @@
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۲.۱ | `DoctorAddress` += `active` + `timezone` | ⏳ | |
|
||||
| ۲.۲ | `timezone` با `DateTimeZone::listIdentifiers()` اعتبارسنجی میشود، نه regex | ⏳ | |
|
||||
| ۲.۳ | `BranchWorkingHours` entity (فرزند aggregate) | ⏳ | |
|
||||
| ۲.۴ | `Room` entity با `TenantOwnedTrait` و جفت مشتق از آدرس در سازنده | ⏳ | نه از بدنهٔ request |
|
||||
| ۲.۵ | `BranchResolver` — تکنقطهٔ uuid آدرس → محیط جاری، ۴۰۴ نه ۴۰۳ | ⏳ | `TenantFilter` روی `doctor_addresses` کار نمیکند |
|
||||
| ۲.۶ | `WorkingHoursService` — اعتبارسنجی کامل **قبل از** حذف (اتمی) | ⏳ | |
|
||||
| ۲.۷ | ساعت با `start_minute`/`end_minute` عددی، نه رشتهٔ `"09:00"` | ⏳ | |
|
||||
| ۲.۸ | `sequence` سمت سرور تخصیص مییابد، نه کلاینت | ⏳ | |
|
||||
| ۲.۹ | `RoomService` با گارد حذف قابل توسعه (آرایهٔ تزریقی، نه زنجیرهٔ `if`) | ⏳ | تسک ۰۲ و ۰۷ گارد اضافه میکنند |
|
||||
| ۲.۱۰ | شش endpoint ساخته شد | ⏳ | صفر endpoint CRUD شعبه — موجود است |
|
||||
| ۲.۱۱ | پزشک مستقل هم شعبه دارد | ⏳ | `type='personal'` از قبل کار میکند |
|
||||
| ۲.۱۲ | کنترلر نازک · `BaseController` · `success/paginated/error` | ⏳ | |
|
||||
| ۲.۱ | `DoctorAddress` += `active` + `timezone` | ✅ | |
|
||||
| ۲.۲ | `timezone` با `DateTimeZone::listIdentifiers()` اعتبارسنجی میشود، نه regex | ✅ | `DateTimeZone::listIdentifiers()` در setter |
|
||||
| ۲.۳ | `BranchWorkingHours` entity (فرزند aggregate) | ✅ | ولی **جفت tenant** گرفت نه فرزند aggregate — ردیف ۳.۵ |
|
||||
| ۲.۴ | `Room` entity با `TenantOwnedTrait` و جفت مشتق از آدرس در سازنده | ✅ | جفت در سازنده از `tenantEntityType/Id()` آدرس |
|
||||
| ۲.۵ | `BranchResolver` — تکنقطهٔ uuid آدرس → محیط جاری، ۴۰۴ نه ۴۰۳ | ✅ | ۴۰۴ میدهد؛ منشی هم پوشش دارد |
|
||||
| ۲.۶ | `WorkingHoursService` — اعتبارسنجی کامل **قبل از** حذف (اتمی) | ✅ | `WorkingHoursTest::testInvalidLaterDayLeavesTheStoredWeekUntouched` |
|
||||
| ۲.۷ | ساعت با `start_minute`/`end_minute` عددی، نه رشتهٔ `"09:00"` | ✅ | |
|
||||
| ۲.۸ | `sequence` سمت سرور تخصیص مییابد، نه کلاینت | ✅ | بعد از `usort` تخصیص مییابد |
|
||||
| ۲.۹ | `RoomService` با گارد حذف قابل توسعه (آرایهٔ تزریقی، نه زنجیرهٔ `if`) | ✅ | `RoomDeletionGuardInterface` + `AutowireIterator` + `_instanceof` |
|
||||
| ۲.۱۰ | شش endpoint ساخته شد | ✅ | هشت شد نه شش: `GET /branches` و `PATCH /branch/{uuid}` هم لازم بودند |
|
||||
| ۲.۱۱ | پزشک مستقل هم شعبه دارد | ✅ | `type=personal` از قبل کار میکرد |
|
||||
| ۲.۱۲ | کنترلر نازک · `BaseController` · `success/paginated/error` | ✅ | |
|
||||
|
||||
## ۳. دیتابیس
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۳.۱ | `branch_working_hours` · `rooms` | ⏳ | |
|
||||
| ۳.۲ | `entity_type, entity_id` ستون **اول** ایندکس `rooms` | ⏳ | |
|
||||
| ۳.۳ | `timezone` روی آدرس از روز اول | ⏳ | افزودن بعدی = backfill زماندار |
|
||||
| ۳.۴ | `rooms.capacity` — ظرفیت همزمان | ⏳ | اتاق سهتخته = یک ردیف با ۳ |
|
||||
| ۳.۵ | `branch_working_hours` در `GlobalTables::AGGREGATE_CHILDREN` با ریشهٔ صریح | ⏳ | |
|
||||
| ۳.۶ | ستونها و جدولها روی `db_test` هم ساخته شد | ⏳ | تاریخچهٔ migration جدا |
|
||||
| ۳.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۳.۸ | `TenantLookupInventoryTest` سبز — repository جدید ثبت شد | ⏳ | |
|
||||
| ۳.۱ | `branch_working_hours` · `rooms` | ✅ | `Version20260730125038` |
|
||||
| ۳.۲ | `entity_type, entity_id` ستون **اول** ایندکس `rooms` | ✅ | `idx_rooms_tenant` و `idx_bwh_tenant` |
|
||||
| ۳.۳ | `timezone` روی آدرس از روز اول | ✅ | افزودن بعدی = backfill زماندار |
|
||||
| ۳.۴ | `rooms.capacity` — ظرفیت همزمان | ✅ | حداقل ۱ در setter و سرویس |
|
||||
| ۳.۵ | `branch_working_hours` در `GlobalTables::AGGREGATE_CHILDREN` با ریشهٔ صریح | ✅ | **رد شد** — ریشه سراسری است، پس جفت واقعی گرفت |
|
||||
| ۳.۶ | ستونها و جدولها روی `db_test` هم ساخته شد | ✅ | دستی، چون `db_test` تاریخچهٔ جدا دارد |
|
||||
| ۳.۷ | `TenantSchemaCoverageTest` سبز | ✅ | |
|
||||
| ۳.۸ | `TenantLookupInventoryTest` سبز — repository جدید ثبت شد | ✅ | سبز بدون نیاز به ثبت تازه |
|
||||
|
||||
## ۴. UI
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | `BranchesPage` · `BranchWorkingHoursPage` · `BranchRoomsPage` | ⏳ | |
|
||||
| ۴.۲ | `DataTable` با skeleton و empty state فارسی | ⏳ | |
|
||||
| ۴.۳ | `PageHeader` با `backTo` روی زیرصفحهها | ⏳ | |
|
||||
| ۴.۴ | هر `select` با `SearchableSelect` — هیچ `<select>` بومی | ⏳ | |
|
||||
| ۴.۵ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
|
||||
| ۴.۶ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن | ⏳ | |
|
||||
| ۴.۷ | دارکمود و حالت فشرده بررسی شد | ⏳ | |
|
||||
| ۴.۸ | RTL و موبایل بررسی شد | ⏳ | |
|
||||
| ۴.۹ | هشدار UI: «هیچ شعبهٔ فعالی باقی نمیماند» | ⏳ | |
|
||||
| ۴.۱۰ | مسیرها در `App.tsx` + ورودی در `SettingsMenuPage` | ⏳ | |
|
||||
| ۴.۱۱ | مجوز موجود `appointment_settings` استفاده شد، نه مجوز تازه | ⏳ | |
|
||||
| ۴.۱ | `BranchesPage` · `BranchWorkingHoursPage` · `BranchRoomsPage` | ✅ | |
|
||||
| ۴.۲ | `DataTable` با skeleton و empty state فارسی | ✅ | |
|
||||
| ۴.۳ | `PageHeader` با `backTo` روی زیرصفحهها | ✅ | `backTo` + breadcrumb روی هر دو زیرصفحه |
|
||||
| ۴.۴ | هر `select` با `SearchableSelect` — هیچ `<select>` بومی | ✅ | `SearchableSelect` برای منطقهٔ زمانی؛ هیچ `<select>` بومی |
|
||||
| ۴.۵ | وضعیت لیست در URL با `useUrlState` | ✅ | جستجو و فیلتر وضعیت در URL |
|
||||
| ۴.۶ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن | ✅ | همه از `var(--…)` |
|
||||
| ۴.۷ | دارکمود و حالت فشرده بررسی شد | 🔄 | کد فقط از توکن استفاده میکند؛ بازبینی چشمی در مرورگر انجام نشد |
|
||||
| ۴.۸ | RTL و موبایل بررسی شد | 🔄 | چیدمان flex/grid با wrap؛ بازبینی چشمی موبایل انجام نشد |
|
||||
| ۴.۹ | هشدار UI: «هیچ شعبهٔ فعالی باقی نمیماند» | ✅ | confirm + title روی تنها شعبهٔ فعال |
|
||||
| ۴.۱۰ | مسیرها در `App.tsx` + ورودی در `SettingsMenuPage` | ✅ | سه مسیر + آیتم منو با `MapPinIcon` |
|
||||
| ۴.۱۱ | مجوز موجود `appointment_settings` استفاده شد، نه مجوز تازه | ✅ | `appointment_settings` بازاستفاده شد |
|
||||
|
||||
## ۵. تست
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۵.۱ | `WorkingHoursTest` — هفت روز، `end<=start`، همپوشانی، `0..1440`، آرایهٔ خالی | ⏳ | |
|
||||
| ۵.۲ | اتمی بودن: بازهٔ نامعتبر در روز ششم → ۴۲۲ و شش روز قبلی دستنخورده | ⏳ | |
|
||||
| ۵.۳ | `RoomCrudTest` — جفت tenant مشتق، `capacity=0` → ۴۲۲ | ⏳ | |
|
||||
| ۵.۴ | آدرس/اتاق محیط دیگر → ۴۰۴ (نه ۴۰۳) | ⏳ | |
|
||||
| ۵.۵ | `BranchAddressFieldsTest` — پیشفرضها، `timezone` نامعتبر → ۴۲۲ | ⏳ | |
|
||||
| ۵.۶ | `phpstan analyse src/Branch` بدون خطا | ⏳ | |
|
||||
| ۵.۱ | `WorkingHoursTest` — هفت روز، `end<=start`، همپوشانی، `0..1440`، آرایهٔ خالی | ✅ | ۱۴ تست |
|
||||
| ۵.۲ | اتمی بودن: بازهٔ نامعتبر در روز ششم → ۴۲۲ و شش روز قبلی دستنخورده | ✅ | |
|
||||
| ۵.۳ | `RoomCrudTest` — جفت tenant مشتق، `capacity=0` → ۴۲۲ | ✅ | ۱۲ تست |
|
||||
| ۵.۴ | آدرس/اتاق محیط دیگر → ۴۰۴ (نه ۴۰۳) | ✅ | هم شعبه و هم اتاق |
|
||||
| ۵.۵ | `BranchAddressFieldsTest` — پیشفرضها، `timezone` نامعتبر → ۴۲۲ | ✅ | ۱۰ تست شامل شمارش کوئری |
|
||||
| ۵.۶ | `phpstan analyse src/Branch` بدون خطا | ✅ | صفر خطا در `src/Branch` |
|
||||
|
||||
## ۶. مستندات
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۶.۱ | `docs/api/branch.md` + ثبت در `docs/api/README.md` | ⏳ | JSON واقعی از curl |
|
||||
| ۶.۲ | «شعبهٔ بدون ساعت کاری = تعریفنشده، نه همیشهباز» نوشته شد | ⏳ | تسک ۰۳ رویش حساب میکند |
|
||||
| ۶.۳ | «`active` در این فاز بیاثر بر اسلات» نوشته شد | ⏳ | |
|
||||
| ۶.۴ | `docs/api/doctor.md` — دو فیلد جدید در پاسخ ۹ endpoint آدرس | ⏳ | تغییر قرارداد است |
|
||||
| ۶.۵ | `docs/architecture/tenancy.md` جدول طبقهبندی بهروز شد | ⏳ | |
|
||||
| ۶.۱ | `docs/api/branch.md` + ثبت در `docs/api/README.md` | ✅ | JSON واقعی از curl روی ddev + ثبت در README |
|
||||
| ۶.۲ | «شعبهٔ بدون ساعت کاری = تعریفنشده، نه همیشهباز» نوشته شد | ✅ | تسک ۰۳ رویش حساب میکند |
|
||||
| ۶.۳ | «`active` در این فاز بیاثر بر اسلات» نوشته شد | ✅ | |
|
||||
| ۶.۴ | `docs/api/doctor.md` — دو فیلد جدید در پاسخ ۹ endpoint آدرس | ✅ | `doctor.md` — تغییر additive روی ۹ اندپوینت |
|
||||
| ۶.۵ | `docs/architecture/tenancy.md` جدول طبقهبندی بهروز شد | ✅ | درسِ «ریشهٔ سراسری، فرزندِ محیطدار» + محدودیت `chosen` |
|
||||
|
||||
## ۷. بازبینی پایانی
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⏳ | |
|
||||
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
|
||||
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۷.۴ | `phpstan` بدون خطای جدید (مقایسه با کامیت پیش از تسک) | ⏳ | |
|
||||
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۷.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ⏳ | |
|
||||
| ۷.۷ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۷.۸ | چکلیست UI کامل | ⏳ | |
|
||||
| ۷.۹ | `nobat724_front` و `clinic-pro-tauri` بررسی شدند | ⏳ | دو فیلد جدید additive است |
|
||||
| ۷.۱۰ | commit، سپس `graphify update .`، سپس commit جدا | ⏳ | |
|
||||
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⚠️ | دو 🔄 مانده، هر دو بازبینی چشمی UI با دلیل مکتوب |
|
||||
| ۷.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۰۶۷ تست / ۲۹۷۴ assertion — صفر خطا |
|
||||
| ۷.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۷.۴ | `phpstan` بدون خطای جدید (مقایسه با کامیت پیش از تسک) | ✅ | ۱۴ خطا قبل و بعد — هیچکدام در فایلهای این تسک |
|
||||
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | `tsc` صفر خطا · vitest ۸۷ فایل / ۶۱۲ تست |
|
||||
| ۷.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ✅ | |
|
||||
| ۷.۷ | `docs/api/*` بهروز | ✅ | |
|
||||
| ۷.۸ | چکلیست UI کامل | ⚠️ | دو ردیف ۴.۷/۴.۸ بازبینی چشمی میخواهند |
|
||||
| ۷.۹ | `nobat724_front` و `clinic-pro-tauri` بررسی شدند | ✅ | هر دو آدرس را مصرف میکنند (`app/doctor/[slug]/page.js` و `OfficeAddressesContent`/`workingDays`/`TurnsTabContent`) ولی فیلدها را **با نام** میخوانند و برای نوشتن payload صریح میسازند (`transformData`) — دو فیلد additive نمیشکندشان. هیچ اندپوینت جدیدی مصرفکننده ندارد |
|
||||
| ۷.۱۰ | commit، سپس `graphify update .`، سپس commit جدا | ✅ | |
|
||||
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | گاردِ حذف اتاق ← تسک ۰۲/۰۷ · گاردِ حذف آدرس ← تسک ۰۷ · اعمال `active` ← تسک ۰۳ |
|
||||
|
||||
Reference in New Issue
Block a user