# معماری — تسک ۰۱

## ساختار فایل

```
src/Branch/
├── Controller/
│   ├── BranchController.php          # CRUD شعبه + ساعت کاری
│   └── RoomController.php            # CRUD اتاق
├── Entity/
│   ├── Branch.php
│   ├── BranchWorkingHours.php
│   └── Room.php
├── Repository/
│   ├── BranchRepository.php
│   ├── BranchWorkingHoursRepository.php
│   └── RoomRepository.php
├── Service/
│   ├── BranchService.php             # ساخت/ویرایش/حذف + قواعد حذف
│   └── WorkingHoursService.php       # اعتبارسنجی و ذخیرهٔ هفت روز
└── Command/
    └── BackfillBranchCommand.php     # app:branch:backfill

assets/admin/pages/
├── BranchesPage.tsx
├── BranchFormPage.tsx                # شامل تب ساعت کاری
└── RoomsPage.tsx
```

## لایه‌بندی

`BranchController` نازک است: اعتبارسنجی ورودی + `EntityContextResolver` + صدا زدن سرویس.
همهٔ قواعد (حذف امن، یکتایی نام در محیط، نرمال‌سازی ساعت) در `BranchService` و
`WorkingHoursService`.

```php
final class BranchService
{
    public function __construct(
        private readonly BranchRepository $branches,
        private readonly RoomRepository   $rooms,
        private readonly EntityManagerInterface $em,
    ) {}

    public function create(EntityContext $ctx, BranchInput $input): Branch
    {
        $branch = new Branch($input->name);
        $branch->assignTenant($ctx);                 // ← اجباری، وگرنه flush می‌شکند
        // ...
    }

    /** حذف فقط وقتی هیچ اتاق یا منبعِ فعالی به شعبه وصل نیست. */
    public function delete(Branch $branch): void
    {
        if ($this->rooms->countActiveByBranch($branch) > 0) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'شعبه دارای اتاق فعال است', 422);
        }
        // ...
    }
}
```

## رابطهٔ Branch با DoctorAddress

`DoctorAddress` حذف نمی‌شود. یک ستون `branch_id` تهی‌پذیر می‌گیرد:

```
DoctorAddress.branch_id ──▶ branches.id   (nullable, ON DELETE SET NULL)
```

دلیل: `location_id` در JSON برنامهٔ هفتگی به `doctor_addresses.id` اشاره دارد و در
`SlotCalculatorService` و `AppointmentController::bookingLocations()` و سایت عمومی مصرف می‌شود.
تغییر آن قرارداد یعنی شکستن سه کلاینت. پس شعبه یک **لایهٔ بالاتر** می‌نشیند و آدرس به آن
لینک می‌شود، نه برعکس.

`BackfillBranchCommand` برای هر محیطی که آدرس دارد یک شعبه با نام آدرس می‌سازد و
`branch_id` را پر می‌کند. dry-run پیش‌فرض، `--force` برای اجرا.

## ساعت کاری شعبه

مثل `WeeklySchedule` یک JSON نیست — جدول جداست، چون تسک ۰۳ باید بتواند
`WHERE branch_id = ? AND day = ?` بزند بدون خواندن و decode کردن JSON برای هر روز از ۹۰ روز.

```php
#[ORM\Entity]
#[ORM\Table(name: 'branch_working_hours')]
#[ORM\UniqueConstraint(name: 'uniq_branch_day_seq', columns: ['branch_id', 'day_of_week', 'sequence'])]
class BranchWorkingHours
{
    private int $dayOfWeek;    // 0=شنبه … 6=جمعه — همان قرارداد SlotCalculatorService
    private int $startMinute;  // دقیقه از نیمه‌شب، 0..1440
    private int $endMinute;
    private int $sequence;     // چند بازه در روز (صبح/عصر)
}
```

`startMinute`/`endMinute` به‌جای رشتهٔ `"08:30"` ذخیره می‌شوند تا مقایسه و تقاطع در تسک ۰۶
حسابی باشد نه رشته‌ای. تبدیل به `H:i` فقط در `toArray()`.

## اتاق

```php
class Room
{
    use TenantOwnedTrait;
    private Branch  $branch;
    private string  $name;
    private ?string $roomType = null;   // متن آزاد — نوعِ اتاق را کلینیک تعریف می‌کند
    private int     $capacity = 1;      // چند بیمار هم‌زمان (اتاق تزریق سه‌تخته = 3)
    private bool    $active = true;
}
```

`capacity` از همین‌جا شروع می‌شود چون مستند بند ۶ صریح می‌گوید سه تخت = **یک منبع با
ظرفیت سه**، نه سه منبع. تسک ۰۲ همین معنا را روی `Resource` تکرار می‌کند و اتاق را
به‌عنوان یک `Resource` با `resource_type=room` منعکس می‌کند.

## پنل ادمین

- `BranchesPage.tsx` — `DataTable` + `PageHeader` با `backTo`، وضعیت لیست در URL با `useUrlState`
- `BranchFormPage.tsx` — دو تب: مشخصات / ساعت کاری. `SearchableSelect` برای شهر
  (هرگز `<select>` بومی)
- `RoomsPage.tsx` — زیرصفحهٔ شعبه، `<BackButton fallback="/admin/branches" />`
- مسیرها در `App.tsx`: `/admin/branches`, `/admin/branches/new`, `/admin/branches/:uuid`,
  `/admin/branches/:uuid/rooms`
