feat(secretary): implement multi-doctor assignment for clinic secretaries

- Added functionality to assign a single secretary to multiple doctors within a clinic, allowing for scoped access to appointments.
- Introduced `SecretaryService` to handle the logic for assigning and syncing doctors for a secretary.
- Updated `SecretaryController` to support multi-doctor assignment via new endpoints and modified existing ones.
- Enhanced `DoctorSecretary` entity to include secretary UUID in its serialized output.
- Implemented repository methods to facilitate the retrieval and management of doctor-secretary relationships.
- Adjusted appointment filtering in `MyAppointmentsController` to ensure secretaries only see appointments for assigned doctors.
- Created tests to validate the new multi-doctor assignment functionality and appointment access restrictions.
- Updated frontend components to support multi-select for doctors in the secretary management UI.
This commit is contained in:
hamed
2026-07-18 08:49:04 +03:30
parent 6ab7ed38b8
commit 1779e0d6de
13 changed files with 947 additions and 51 deletions
+81 -2
View File
@@ -14,6 +14,7 @@
- یک منشی می‌تواند هم در مطب شخصی یک دکتر و هم در کلینیک همان دکتر فعال باشد (دو ردیف مجزا)
- منشی کلینیک می‌تواند به چند دکتر در همان کلینیک متصل باشد
- scope فعال در runtime از جدول `user_active_context` (db_uuid) خوانده می‌شود
- **محدودسازی به پزشکانِ تخصیص‌یافته:** منشیِ کلینیک فقط نوبت‌های پزشکانی را می‌بیند/رزرو می‌کند که واقعاً به او تخصیص داده شده‌اند — نه همه‌ی پزشکان کلینیک. لیست نوبت (`GET /api/v1/my/appointments`) با `a.doctor IN (پزشکانِ تخصیص‌یافته)` فیلتر می‌شود و گیت رزرو (`POST /api/v1/my/appointment`) رابطه‌ی فعالِ همان (منشی، کلینیک، پزشک) را چک می‌کند. permission رزرو از همان ردیفِ پزشک خوانده می‌شود
Secretaries are linked to a doctor and have granular permissions controlling what they can do on behalf of the doctor.
@@ -76,7 +77,8 @@ Create a secretary for a doctor.
| Field | Type | Required | Description |
| --------------- | ------------- | -------- | -------------------------------------------- |
| `doctor_uuid` | string (UUID) | ✅ | Doctor to assign secretary to |
| `doctor_uuid` | string (UUID) | ✅\* | Single doctor to assign (legacy/doctor flow) |
| `doctor_uuids` | string[] (UUID) | ✅\* | **Clinic only** — assign one secretary to several clinic doctors at once. When present (non-empty) and caller is `ROLE_CLINIC`, this multi-doctor path is used instead of `doctor_uuid` |
| `mobile_number` | string | ✅ | Secretary's login mobile |
| `name` | string | ❌ | Full name (نام + نام خانوادگی) → `user_name` |
| `national_code` | string | ❌ | کد ملی منشی (nullable) |
@@ -84,6 +86,25 @@ Create a secretary for a doctor.
| `password` | string | ❌ | Initial password (auto-generated if omitted) |
| `permissions` | object | ❌ | Permission set (see structure below) |
\* یکی از `doctor_uuid` (تک‌پزشکی) یا `doctor_uuids` (چند‌پزشکیِ کلینیک) الزامی است.
**پاسخِ حالت چند‌پزشکی (`doctor_uuids` + `ROLE_CLINIC`) — `201`:**
```json
{
"success": true,
"data": {
"secretary_uuid": "550e8400-...",
"created": [ { "uuid": "...", "secretary_uuid": "...", "doctor_uuid": "...", "...": "..." } ],
"skipped_duplicate": [],
"skipped_limit": [],
"skipped_not_in_clinic": []
}
}
```
- `created`: ردیف‌های تازه‌ساخته/فعال‌شده · `skipped_duplicate`: قبلاً متصل بوده · `skipped_limit`: سقفِ پلنِ آن پزشک پر است · `skipped_not_in_clinic`: پزشک عضو کلینیک نیست. حلقه اتمیک است و بقیه‌ی پزشکان ادامه می‌یابند.
**Permissions Structure:**
مجموعهٔ منابع (resources) بر اساس صفحات موجود پنل ادمین است. `mergePermissions` هر منبع/اکشن ارسال‌شده را deep-merge می‌کند؛ فقط `appointments` در بک‌اند enforce می‌شود (`MyAppointmentsController`, `DashboardController`)، بقیه UI/ذخیره‌ای هستند.
@@ -344,6 +365,7 @@ Get all secretaries across **all doctors** of a clinic.
"data": [
{
"uuid": "...",
"secretary_uuid": "...",
"user_name": "علی محمدی",
"mobile_number": "09...",
"doctor_name": "دکتر احمد رضایی",
@@ -360,7 +382,7 @@ Get all secretaries across **all doctors** of a clinic.
- این endpoint فقط منشی های را برمی‌گرداند که با `owner_type='clinic'` تعریف شده‌اند
- منشی های که خود دکتر (با `owner_type='doctor'`) تعریف کرده از این لیست مخفی هستند
- یک منشی می‌تواند به چند دکتر در همان کلینیک متصل باشد — در لیست چندبار ظاهر می‌شود (یک ردیف به ازای هر دکتر)
- یک منشی می‌تواند به چند دکتر در همان کلینیک متصل باشد — در لیست چندبار ظاهر می‌شود (یک ردیف به ازای هر دکتر). برای گروه‌بندی «یک منشی، چند پزشک» از `secretary_uuid` (uuid کاربرِ منشی) استفاده کنید
### Errors
@@ -372,6 +394,63 @@ Get all secretaries across **all doctors** of a clinic.
---
## PUT `/api/v1/secretaries/clinic/{clinicUuid}/doctors`
هم‌گام‌سازی مجموعه‌ی پزشکانِ یک منشیِ کلینیک (owner_type='clinic'): پزشکانِ خواسته‌شده افزوده/فعال و بقیه غیرفعال می‌شوند. برای «افزودن/حذف پزشک از یک منشی موجود» بدون تغییر ساختاری.
**Permission:** `ROLE_CLINIC` (must own clinic) | `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
| ------------ | ------------- | ----------- |
| `clinicUuid` | string (UUID) | Clinic UUID |
### Request Body (`application/json`)
```json
{
"secretary_uuid": "550e8400-...",
"doctor_uuids": ["uuid-doc-a", "uuid-doc-b"]
}
```
| Field | Type | Required | Description |
| ---------------- | --------------- | -------- | ------------------------------------------------------------ |
| `secretary_uuid` | string (UUID) | ✅ | uuid کاربرِ منشی (همان `secretary_uuid` خروجی لیست/ساخت) |
| `doctor_uuids` | string[] (UUID) | ✅ | مجموعه‌ی نهاییِ پزشکان؛ نبودها افزوده، اضافه‌ها غیرفعال می‌شوند |
### Response `200`
```json
{
"success": true,
"data": {
"added": 1,
"removed": 1,
"skipped_limit": [],
"skipped_not_in_clinic": []
}
}
```
| Field | Type | Description |
| ----------------------- | -------- | ------------------------------------------------ |
| `added` | int | تعداد ردیف‌های افزوده/فعال‌شده |
| `removed` | int | تعداد ردیف‌های غیرفعال‌شده |
| `skipped_limit` | string[] | uuid پزشکانی که به سقفِ پلن رسیده‌اند (نادیده گرفته) |
| `skipped_not_in_clinic` | string[] | uuid پزشکانی که عضو این کلینیک نیستند |
### Errors
| Code | HTTP | Description |
| -------------------- | ---- | ------------------------------------ |
| `ERR_AUTH_006` | 403 | Not clinic owner nor admin |
| `ERR_VALIDATION_001` | 422 | `secretary_uuid`/`doctor_uuids` missing |
| `ERR_VALIDATION_002` | 404 | Clinic or secretary not found |
---
## محدودیت پنل اشتراکی
تعداد منشی‌های مجاز بر اساس پنل فعال doctor تعیین می‌شود: