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

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

```
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` یا `<BackButton />` دارند
