# معماری — تسک ۰۱ ## ساختار فایل ``` 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` برای شهر (هرگز `