# تسک ۰۲ — مدل منبع: نوع منبع، منبع، مهارت، استخر

**فاز:** ۱ (هسته) · **وابستگی:** ۰۱ · **زمان:** ۱۴-۱۸ ساعت

---

## هدف

قانون طلایی اول مستند: «تقویم مال منبع است، نه مال پزشک». امروز تنها موجودیتی که
می‌تواند اشغال شود پزشک است و پرسنل (`ClinicStaff`) فقط یک برچسب روی سرویس و نوبت است.
این تسک لایهٔ منبع را می‌سازد: هر چیزی که ممکن است اشغال باشد — پزشک، اپراتور، دستیار،
دستگاه، اتاق، تخت، یونیت.

## وضعیت فعلی

```php
// src/Staff/Entity/ClinicStaff.php — تنها «منبع» امروز
private string  $fullName;
private ?string $jobTitle;      // متن آزاد، بدون معنای ساختاری
private bool    $active;
private ?User   $user;          // برای ورود به پنل
```

بدون تقویم، بدون ظرفیت، بدون مهارت. `ServiceItem.staffMembers` یک ManyToMany به همین است
و `Appointment.staff_id` یک ارجاع تکی — هیچ‌کدام تداخل زمانی را بررسی نمی‌کنند.

## دامنه

**هست:** `ResourceType`، `Resource`، `Skill`، `ResourceSkill`، `ResourcePool`،
`ResourcePoolMember`، ویژگی‌های آزاد (`attributes` JSON)، ظرفیت هم‌زمان،
زمان آماده‌سازی/تمیزکاری per منبع، CRUD پنل، و **پل زدن `ClinicStaff` و `Doctor` و `Room`
به `Resource`**.

**نیست:** تقویم و مرخصی (تسک ۰۳)، نیازمندی منبع per بخش (تسک ۰۵)، محاسبهٔ اشغال (تسک ۰۶/۰۷).

## Endpoint ها

| متد | مسیر | توضیح |
|---|---|---|
| GET/POST | `/api/v1/resource-types` | نوع منبع (کلینیک خودش تعریف می‌کند) |
| PATCH/DELETE | `/api/v1/resource-type/{uuid}` | |
| GET | `/api/v1/resources` | لیست با فیلتر `branch_uuid`, `type_uuid`, `active` |
| POST | `/api/v1/resource` | ساخت منبع |
| GET/PATCH/DELETE | `/api/v1/resource/{uuid}` | |
| GET/POST | `/api/v1/skills` | مهارت‌ها |
| PATCH/DELETE | `/api/v1/skill/{uuid}` | |
| PUT | `/api/v1/resource/{uuid}/skills` | جایگزینی کامل مهارت‌های منبع |
| GET/POST | `/api/v1/resource-pools` | استخر منابع قابل جایگزینی |
| PATCH/DELETE | `/api/v1/resource-pool/{uuid}` | |
| PUT | `/api/v1/resource-pool/{uuid}/members` | جایگزینی کامل اعضا |

## معیار پذیرش

- ✅ موفق: کلینیک نوع منبع «دستگاه لیزر» می‌سازد، سه منبع از آن نوع در شعبهٔ مرکزی ثبت
  می‌کند، یک استخر «لیزرهای آلکساندرایت» می‌سازد و هر سه را عضو می‌کند →
  `GET /api/v1/resource-pool/{uuid}` هر سه را با `branch_uuid` برمی‌گرداند.
- ✅ موفق: مهارت «لیزر آلکساندرایت» ساخته و به دو اپراتور داده می‌شود →
  `GET /api/v1/resources?skill_uuid=…` فقط همان دو را برمی‌گرداند.
- ✅ موفق: بعد از اجرای `app:resource:backfill`، هر `ClinicStaff` فعال یک `Resource` با
  `type=staff` و هر `Doctor` دارای برنامه یک `Resource` با `type=doctor` دارد.
- ❌ خطا: منبع با `branch_uuid` متعلق به محیط دیگر → `404`.
- ❌ خطا: عضو کردن منبعی از شعبهٔ A در استخری که منابعش در شعبهٔ B هستند → `422`
  «همهٔ اعضای استخر باید در یک شعبه باشند».
- ⚠️ مرزی: `capacity = 3` روی اتاق تزریق → یک ردیف، نه سه. `GET` مقدار ۳ را برمی‌گرداند.
- ⚠️ مرزی: `setup_minutes = 0, cleanup_minutes = 10` → معتبر.
- ⚠️ مرزی: حذف مهارتی که به منبعی داده شده → `422`؛ باید اول از منابع برداشته شود.
- ⚠️ مرزی: `attributes` با کلید ناشناخته → پذیرفته می‌شود (عمداً آزاد)، ولی مقدار غیر
  اسکالر → `422`.

## خروجی

- `src/Resource/` کامل با تست
- صفحات پنل: `ResourcesPage`, `ResourceFormPage`, `ResourceTypesPage`, `SkillsPage`, `ResourcePoolsPage`
- `docs/api/resource.md`
- `app:resource:backfill` (dry-run پیش‌فرض)
