# معماری — تسک ۰۲ ## ساختار فایل ``` src/Resource/ ├── Controller/ │ ├── ResourceController.php │ ├── ResourceTypeController.php │ ├── SkillController.php │ └── ResourcePoolController.php ├── Entity/ │ ├── ResourceType.php │ ├── ClinicResource.php # نامِ کلاس عمداً Resource نیست (تداخل با کلمهٔ رزرو PHP نیست، ولی با Symfony/Doctrine ابهام دارد) │ ├── Skill.php │ ├── ResourceSkill.php │ ├── ResourcePool.php │ └── ResourcePoolMember.php ├── Repository/… ├── Service/ │ ├── ResourceService.php │ ├── SkillAssignmentService.php │ ├── ResourcePoolService.php │ └── ResourceLinker.php # پل بین Doctor/ClinicStaff/Room و ClinicResource └── Command/ └── BackfillResourceCommand.php ``` ## `ClinicResource` ```php #[ORM\Entity(repositoryClass: ClinicResourceRepository::class)] #[ORM\Table(name: 'clinic_resources')] #[ORM\Index(columns: ['entity_type', 'entity_id', 'active'], name: 'idx_resources_tenant')] class ClinicResource { use TenantOwnedTrait; private string $uuid; private Branch $branch; // منابع همیشه مال شعبه‌اند — فیزیکی‌اند private ResourceType $type; private string $name; private int $capacity = 1; // ظرفیت هم‌زمان private int $setupMinutes = 0; private int $cleanupMinutes = 0; private array $attributes = []; // JSON آزاد: gender, device_model, floor private bool $active = true; // ── پل به موجودیت‌های موجود؛ حداکثر یکی غیر-null است ── private ?Doctor $doctor = null; private ?ClinicStaff $staff = null; private ?Room $room = null; } ``` ### چرا پل، نه ادغام `Doctor` و `ClinicStaff` و `Room` هرکدام هویت مستقل و مصرف‌کنندهٔ زنده دارند (`appointments.doctor_id`، `service_item_staff`، سایت عمومی). تبدیل آن‌ها به زیرکلاس `Resource` یعنی مهاجرت هم‌زمان همهٔ آن مسیرها. به‌جایش: ``` Doctor 1 ──0..1 ClinicResource (type=doctor) ClinicStaff 1 ──0..1 ClinicResource (type=staff) Room 1 ──0..1 ClinicResource (type=room) ClinicResource بدون پل = دستگاه/تجهیزات ``` `ResourceLinker` تنها نقطه‌ای است که این نگاشت را می‌داند: ```php final class ResourceLinker { /** منبعِ متناظر با یک پرسنل؛ اگر نبود می‌سازد. */ public function forStaff(ClinicStaff $staff): ClinicResource { … } public function forDoctor(Doctor $doctor, Branch $branch): ClinicResource { … } /** برعکس: منبع → موجودیت اصلی، برای نمایش در UI. */ public function subject(ClinicResource $r): Doctor|ClinicStaff|Room|null { … } } ``` هیچ سرویس دیگری نباید مستقیم `$resource->getStaff()` را برای تصمیم‌گیری بخواند. ## مهارت‌ها — جدول، نه قانون مستند بند ۶ صریح است: «کدام اپراتور مجاز است با کدام دستگاه کار کند» یک **اطلاعات** است، نه یک قانون. با ۵۰ اپراتور و ۲۰۰ سرویس، سپردنش به موتور قوانین یعنی ۱۰٬۰۰۰ قانون. ``` skills (uuid, name, tenant) resource_skills (resource_id, skill_id, level) ← جدول واسط ساده ``` `level` (۱..۵) از روز اول هست چون تسک ۰۶ استراتژی «حفظ متخصص‌ها» را روی همین می‌سازد و افزودنش بعداً یعنی backfill با حدس. ## استخر منابع ```php class ResourcePool { use TenantOwnedTrait; private Branch $branch; // استخر درون یک شعبه است — منبعِ شعبهٔ دیگر جایگزین نیست private ResourceType $type; // اعضا باید هم‌نوع باشند private string $name; private Collection $members; // ResourcePoolMember } ``` قاعدهٔ اعتبار در `ResourcePoolService::replaceMembers()`: همهٔ اعضا باید `branch` و `type` یکسان با خود استخر داشته باشند، وگرنه `422`. دلیل: تسک ۰۶ فرض می‌کند «هر عضو استخر جایگزین کامل دیگری است» — اگر یکی در شعبهٔ دیگری باشد، بیمار در ساختمان اشتباه می‌ایستد. ## اعتبارسنجی `attributes` JSON آزاد است ولی نه بی‌قید: ```php // ResourceService::normalizeAttributes() // - کلید: [a-z_]{1,40} // - مقدار: string|int|bool|float فقط — نه آرایه، نه object // - حداکثر ۲۰ کلید ``` دلیل محدودیت اسکالر: تسک ۰۵ قید `same_gender` و تسک ۰۹ شرط‌های منبع را روی همین مقادیر با مقایسهٔ ساده می‌سنجند. آرایهٔ تودرتو یعنی مقایسهٔ دلخواه، یعنی همان چیزی که مستند بند ۸ ممنوع کرده. کلیدهای شناخته‌شده (قرارداد، نه اجبار): `gender`, `device_model`, `floor`, `brand`. ## پنل ادمین - `ResourcesPage.tsx` — `DataTable` با فیلتر شعبه/نوع/فعال، همه در URL (`useUrlState`) - `ResourceFormPage.tsx` — `SearchableSelect` برای شعبه و نوع، چیپ برای مهارت‌ها، `PriceInput` لازم نیست، ولی برای `setup/cleanup` عدد ساده با پسوند «دقیقه» - `SkillsPage.tsx`, `ResourceTypesPage.tsx`, `ResourcePoolsPage.tsx` — لیست‌های ساده - همهٔ زیرصفحه‌ها `backTo` یا `` دارند