# Staff API

مدیریت پرسنل مطب/کلینیک (بدون حذف — فقط toggle فعال/غیرفعال).

**هر پرسنل حساب کاربری ورود دارد.** هنگام ایجاد/ویرایش، یک `User` با نقش `ROLE_STAFF` ساخته
(یا کاربر موجودِ همان موبایل استفاده) و به ردیف پرسنل وصل می‌شود؛ بنابراین `phone` اجباری و
معتبر (`^09\d{9}$`) است و همان **نام‌کاربری ورود** است. تغییر `phone` یعنی تغییر نام‌کاربری.

چنین کاربری در `/admin` وارد می‌شود، ولی دسترسی‌اش به `GET /api/v1/dashboard/staff` و چند
مسیر حساب کاربری محدود است (بقیهٔ `/api/v1/*` برای او ۴۰۳ است — رجوع به
[dashboard.md](dashboard.md) و [auth.md](auth.md)). قطع دسترسی با
`PATCH /api/v1/staff/{uuid}/toggle` انجام می‌شود، نه با حذف حساب.

> ردیف‌های پرسنلِ ساخته‌شده پیش از این تغییر ممکن است `has_account: false` باشند؛ با اولین
> ویرایش (که `phone` معتبر می‌خواهد) صاحب حساب می‌شوند.

---

## GET /api/v1/staff

دریافت لیست پرسنل entity جاری (از JWT).

**Permission:** `IS_AUTHENTICATED_FULLY` (ROLE_DOCTOR یا ROLE_CLINIC)

**Response 200:**
```json
{
  "success": true,
  "data": [
    {
      "uuid": "a1b2c3d4-...",
      "entity_type": "clinic",
      "entity_id": 5,
      "full_name": "علی محمدی",
      "phone": "09121234567",
      "job_title": "منشی",
      "address": null,
      "national_code": "0012345678",
      "active": true,
      "has_account": false,
      "user_uuid": null,
      "created_at": 1718000000,
      "updated_at": 1718000000
    }
  ]
}
```

| فیلد | نوع | توضیح |
|------|-----|-------|
| has_account | bool | حساب ورود دارد یا نه — برای ردیف‌های جدید همیشه `true`؛ `false` فقط در ردیف‌های قدیمیِ پیش از این قابلیت |
| user_uuid | string\|null | uuid کاربرِ متصل؛ `null` یعنی حساب ندارد |

---

## POST /api/v1/staff

ایجاد پرسنل جدید.

**Permission:** `IS_AUTHENTICATED_FULLY` (ROLE_DOCTOR یا ROLE_CLINIC)

**Request Body:**
```json
{
  "full_name": "محمد رحیمی",
  "phone": "09121110002",
  "job_title": "پرستار",
  "address": "تهران، خیابان ولیعصر",
  "national_code": "0012345678",
  "password": "Staff@1234"
}
```

| فیلد | نوع | الزامی |
|------|-----|--------|
| full_name | string | ✅ |
| phone | string — `^09\d{9}$`، ارقام فارسی به لاتین تبدیل می‌شوند | ✅ نام‌کاربری ورود |
| job_title | string | ❌ |
| address | string | ❌ |
| national_code | string(15) — ارقام فارسی به لاتین تبدیل می‌شوند | ❌ |
| password | string | ❌ — رمز ورود؛ خالی بگذارید تا کاربر با «فراموشی رمز» تعیینش کند. روی کاربر موجود، رمز فعلی پاک نمی‌شود |

اگر موبایل قبلاً `User` داشته باشد، کاربر جدید ساخته نمی‌شود؛ فقط `ROLE_STAFF` به نقش‌هایش
اضافه و به این ردیف پرسنل وصل می‌شود.

**Response 201** (خروجی واقعی):
```json
{
  "success": true,
  "data": {
    "uuid": "d64826bc-5e5d-4df6-83bb-3ddc70a99636",
    "entity_type": "doctor",
    "entity_id": 1,
    "full_name": "محمد رحیمی",
    "phone": "09121110002",
    "job_title": "پرستار",
    "address": null,
    "national_code": null,
    "active": true,
    "has_account": true,
    "user_uuid": "4d79b9b0-f330-4dea-9f02-e38fc716b115",
    "created_at": 1785393740,
    "updated_at": 1785393740
  }
}
```

**Errors:**
| Code | HTTP | توضیح |
|------|------|-------|
| ERR_VALIDATION_001 | 422 | full_name خالی است |
| ERR_STAFF_MOBILE_INVALID | 422 | شماره خالی/نامعتبر است یا شمارهٔ خودِ مالک محیط است (`field: "phone"`) |
| ERR_STAFF_MOBILE_TAKEN | 409 | در همین محیط، پرسنل دیگری با این شماره ثبت شده است (`field: "phone"`) |
| ERR_FORBIDDEN_001 | 403 | پروفایل doctor/clinic یافت نشد |

خروجی واقعی خطاها:
```json
{"success":false,"data":null,"errors":[{"code":"ERR_STAFF_MOBILE_INVALID","message":"شماره موبایل پرسنل معتبر نیست","field":"phone"}]}
{"success":false,"data":null,"errors":[{"code":"ERR_STAFF_MOBILE_TAKEN","message":"برای این شماره قبلاً پرسنلی ثبت شده است","field":"phone"}]}
```

---

## PATCH /api/v1/staff/{uuid}

ویرایش اطلاعات پرسنل.

**Permission:** `IS_AUTHENTICATED_FULLY` — فقط owner یا ROLE_ADMIN

**Request Body (همه فیلدها اختیاری):**
```json
{
  "full_name": "علی محمدی ویرایش‌شده",
  "phone": "09129999999",
  "job_title": "منشی ارشد",
  "address": null,
  "national_code": null,
  "password": "NewPass@123"
}
```

ویرایش هم حساب را می‌سازد/به‌روز می‌کند: اگر `phone` ارسال نشود، شمارهٔ فعلی همان ردیف
استفاده می‌شود؛ اگر شمارهٔ جدید بیاید، نام‌کاربری ورود عوض می‌شود. `password` خالی رمز فعلی
را پاک نمی‌کند.

**Response 200:** همان ساختار staff object

**Errors:**
| Code | HTTP | توضیح |
|------|------|-------|
| ERR_STAFF_NOT_FOUND | 404 | پرسنل یافت نشد |
| ERR_STAFF_MOBILE_INVALID | 422 | شمارهٔ نامعتبر یا شمارهٔ مالک محیط |
| ERR_STAFF_MOBILE_TAKEN | 409 | شماره در همین محیط قبلاً ثبت شده |
| ERR_FORBIDDEN_001 | 403 | دسترسی ندارید |

---

## PATCH /api/v1/staff/{uuid}/toggle

تغییر وضعیت فعال/غیرفعال پرسنل (soft toggle — هیچ حذفی انجام نمی‌شود).

**Permission:** `IS_AUTHENTICATED_FULLY` — فقط owner یا ROLE_ADMIN

**Response 200:**
```json
{
  "success": true,
  "data": {
    "uuid": "a1b2c3d4-...",
    "active": false,
    ...
  }
}
```

**Errors:**
| Code | HTTP | توضیح |
|------|------|-------|
| ERR_STAFF_NOT_FOUND | 404 | پرسنل یافت نشد |
| ERR_FORBIDDEN_001 | 403 | دسترسی ندارید |

---

## رابطه با منبع

پرسنل به‌تنهایی تقویم، ظرفیت و مهارت ندارد؛ آن‌ها روی **منبع** می‌نشینند
([resource.md](resource.md)). هر پرسنل می‌تواند یک منبع از نوع `staff` در هر شعبه
داشته باشد و `app:resource:backfill` این پل را برای پرسنلِ موجود می‌سازد.

| نکته | چرا |
|---|---|
| `job_title` **مهارت نیست** | متن آزاد و فقط برای نمایش است؛ هیچ‌جا برای تصمیم‌گیری parse نمی‌شود. مهارت یک موجودیت با هویت است که در انتخاب منبع و شرط قوانین استفاده می‌شود |
| غیرفعال کردن پرسنل | باید منبعش را هم ببندد، وگرنه در جستجوی وقت ظاهر می‌شود. `ResourceLinker::syncActive()` این کار را می‌کند — **فراخوانی صریح**، نه Doctrine lifecycle callback (که در `getArrayResult()` اجرا نمی‌شود و رفتار نامتقارن می‌سازد) |
| غیرفعال کردن منبع | پرسنل را غیرفعال **نمی‌کند** — پرسنل ممکن است فقط نقش اداری داشته باشد |
| پرسنلِ محیط چندشعبه‌ای | backfill پلش را نمی‌سازد و **گزارش** می‌دهد: هیچ ستونی نمی‌گوید در کدام شعبه کار می‌کند و حدس زدن او را در ساختمان اشتباه می‌نشاند |

ظرفیت منبعِ پرسنل همیشه ۱ است — یک شخص هم‌زمان دو بیمار ندارد.
