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:
hamed
2026-07-30 16:48:49 +03:30
co-authored by Claude Opus 5
parent eebb363b9f
commit d813843fcd
15 changed files with 1472 additions and 63 deletions
+15 -2
View File
@@ -117,7 +117,7 @@ class BranchController extends BaseController
'branch_uuid' => $address->getUuid(),
'timezone' => $address->getTimezone(),
'defined' => $this->workingHours->isDefined($address),
'days' => $this->workingHours->read($address),
'days' => self::daysObject($this->workingHours->read($address)),
]);
}
@@ -142,7 +142,20 @@ class BranchController extends BaseController
'branch_uuid' => $address->getUuid(),
'timezone' => $address->getTimezone(),
'defined' => $days !== array_fill_keys(WorkingHoursService::DAYS, []),
'days' => $days,
'days' => self::daysObject($days),
]);
}
/**
* کلیدهای ۰..۶ پشت‌سرهم‌اند، پس json_encode آرایهٔ PHP را به **آرایهٔ JSON**
* تبدیل می‌کرد نه به شیئی با کلیدهای "0".."6". کلاینت با `days["0"]` هر دو را
* می‌خواند، ولی شکل پاسخ ناپایدار می‌شد: کافی بود یک روز جا بیفتد تا همان فیلد
* شیء برگردد. (کلید رشته‌ایِ عددی هم چاره نیست — PHP خودش به int برش می‌گرداند.)
*
* @param array<int, list<array<string, mixed>>> $days
*/
private static function daysObject(array $days): \stdClass
{
return (object) $days;
}
}