refactor(resource): drop the branch domain from resources

Resources never needed a branch: devices and rooms belong to the clinic
itself, and the picker always had exactly one option — a mandatory click
that decided nothing.

- `address_uuid` is now optional on resource and pool creation; when it is
  missing the environment's own address is used. Clients still sending it
  keep working.
- The panel no longer asks for or displays a branch anywhere: resource
  form, list column and filter, pool form and column, detail row, and the
  resource-first booking page.
- Availability no longer gates on `doctor_addresses.active`. That gate shut
  down every device of a clinic whose address row happened to be inactive,
  with a message no page in the panel could act on — no endpoint writes
  that column at all.

`address_id` stays on the resource: the timezone and the tenant pair are
derived from it. It is simply no longer the user's decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-03 14:57:47 +03:30
co-authored by Claude Opus 5
parent 05bb4a4ade
commit 581a553516
19 changed files with 246 additions and 117 deletions
@@ -73,8 +73,8 @@ class ResourceController extends BaseController
$data = json_decode($request->getContent(), true);
if (!is_array($data) || !is_string($data['address_uuid'] ?? null)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد address_uuid الزامی است', 422, 'address_uuid');
if (!is_array($data)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422);
}
if (!is_string($data['type_uuid'] ?? null)) {
@@ -87,7 +87,11 @@ class ResourceController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'انتخاب پزشک ناظر الزامی است', 422, 'supervisor_doctor_uuid');
}
$address = $this->context->address($user, $data['address_uuid']);
// شعبه از کاربر پرسیده نمی‌شود؛ آدرسِ خودِ محیط مبناست. `address_uuid` فقط برای
// سازگاری با کلاینت‌های قدیمی پذیرفته می‌شود.
$address = is_string($data['address_uuid'] ?? null) && $data['address_uuid'] !== ''
? $this->context->address($user, $data['address_uuid'])
: $this->context->defaultAddress($user);
$type = $this->context->type($user, $data['type_uuid']);
$supervisor = $this->context->supervisor($user, $data['supervisor_doctor_uuid']);
@@ -50,8 +50,8 @@ class ResourcePoolController extends BaseController
$data = json_decode($request->getContent(), true);
if (!is_array($data) || !is_string($data['address_uuid'] ?? null)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد address_uuid الزامی است', 422, 'address_uuid');
if (!is_array($data)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422);
}
if (!is_string($data['type_uuid'] ?? null)) {
@@ -64,8 +64,11 @@ class ResourcePoolController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نام استخر الزامی است', 422, 'name');
}
// مثل منبع: شعبه پرسیده نمی‌شود و آدرسِ خودِ محیط مبناست.
$pool = new ResourcePool(
$this->context->address($user, $data['address_uuid']),
is_string($data['address_uuid'] ?? null) && $data['address_uuid'] !== ''
? $this->context->address($user, $data['address_uuid'])
: $this->context->defaultAddress($user),
$this->context->type($user, $data['type_uuid']),
$name,
);
@@ -171,9 +171,13 @@ final class ResourceAvailabilityService
return new DayAvailability($midnight, $dayOfWeek, [], ['resource_inactive']);
}
if (!$resource->getAddress()->isActive()) {
return new DayAvailability($midnight, $dayOfWeek, [], ['address_inactive']);
}
/**
* فعال‌بودنِ آدرس اینجا سنجیده **نمی‌شود**.
*
* منابع دامنهٔ شعبه ندارند: آدرس فقط حاملِ منطقهٔ زمانی و جفتِ محیط است و هیچ
* جای پنل هم روشن/خاموشش نمی‌کند. وقتی می‌شد، یک ردیفِ قدیمیِ `active = 0` کلِ
* دستگاه‌های کلینیک را با پیامی خاموش می‌کرد که کاربر راهی برای رفعش نداشت.
*/
$override = $overrideMap[$midnight] ?? null;
$holiday = $holidayMap[$midnight] ?? null;
+25
View File
@@ -54,6 +54,31 @@ final class ResourceContext
return $this->branches->resolve($user, $addressUuid);
}
/**
* آدرسِ خودِ محیط — مطب یا کلینیک.
*
* منابع دامنهٔ «شعبه» ندارند: دستگاه و اتاق مالِ همین کلینیک‌اند و پرسیدنِ شعبه از
* کاربر یک انتخابِ همیشه‌یکسان بود. آدرس همچنان نگه داشته می‌شود چون منطقهٔ زمانی و
* جفتِ محیطِ منبع از آن مشتق می‌شوند، ولی کاربر دیگر انتخابش نمی‌کند.
*
* محیطی که چند آدرس دارد، اولینش را می‌گیرد؛ ترتیب از خودِ مخزن می‌آید و پایدار است.
*/
public function defaultAddress(User $user): DoctorAddress
{
$addresses = $this->branches->listForContext($user);
if ($addresses === []) {
throw new AppException(
ErrorCodes::ERR_VALIDATION_002,
'برای این محیط آدرسی ثبت نشده است — ابتدا آدرس کلینیک را کامل کنید',
422,
'address_uuid',
);
}
return $addresses[array_key_first($addresses)];
}
public function type(User $user, string $uuid): ResourceType
{
return $this->owned($user, $this->types->findByUuid($uuid), 'نوع منبع یافت نشد');
+1 -1
View File
@@ -16,7 +16,7 @@ final readonly class DayAvailability
* @param list<TimeInterval> $intervals بازه‌های آزاد، به‌صورت timestamp مطلق
* @param list<string> $reasons `national_holiday`، `tenant_holiday`، `no_shift`،
* `branch_closed`، `outside_branch_hours`، `exception`،
* `resource_inactive`، `branch_inactive`
* `resource_inactive`
*/
public function __construct(
public int $date,