Render the clinic services page (sections → services) inside the settings sub-navigation shell (SettingsLayout, "خدمات" active) to match the Figma settings design. Restyle section cards to show the service count and a status toggle with edit/delete actions, and service cards with labelled price/duration and personnel chips. A service can now have multiple personnel: add an additive many-to-many ServiceItem↔ClinicStaff (staffMembers, EAGER) while keeping the legacy single `staff` column mirrored for backward compatibility. Endpoints accept `staff_uuids[]` (falling back to the legacy single `staff_uuid`) and return `staff_members[]`; the section list now reports `items_count`. Backfill-safe: pre-migration rows fall back to the single staff in toArray. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
235 lines
7.6 KiB
Markdown
235 lines
7.6 KiB
Markdown
# Clinic Services API
|
|
|
|
مدیریت بخشها و سرویسهای کلینیک/مطب.
|
|
|
|
**نیاز به پنل:** Basic یا بالاتر (`ERR_SUBSCRIPTION_REQUIRED` اگر نداشت)
|
|
|
|
---
|
|
|
|
## GET /api/v1/service-sections
|
|
|
|
لیست بخشهای سرویس entity جاری.
|
|
|
|
**Permission:** `IS_AUTHENTICATED_FULLY` + پنل Basic+
|
|
|
|
**Response 200:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"uuid": "...",
|
|
"entity_type": "clinic",
|
|
"entity_id": 5,
|
|
"name": "آزمایشگاه",
|
|
"active": true,
|
|
"items_count": 10,
|
|
"created_at": 1718000000,
|
|
"updated_at": 1718000000
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
> `items_count` تعداد سرویسهای همان بخش است (فقط در این endpoint لیستی برگردانده میشود).
|
|
|
|
---
|
|
|
|
## POST /api/v1/service-section
|
|
|
|
ایجاد بخش جدید.
|
|
|
|
**Permission:** `IS_AUTHENTICATED_FULLY` + پنل Basic+
|
|
|
|
**Request Body:**
|
|
```json
|
|
{ "name": "رادیولوژی" }
|
|
```
|
|
|
|
**Response 201:** ServiceSection object
|
|
|
|
**Errors:**
|
|
| Code | HTTP | توضیح |
|
|
|------|------|-------|
|
|
| ERR_SUBSCRIPTION_REQUIRED | 403 | نیاز به پنل Basic+ |
|
|
| ERR_VALIDATION_001 | 422 | name خالی است |
|
|
|
|
---
|
|
|
|
## PATCH /api/v1/service-section/{uuid}
|
|
|
|
ویرایش بخش.
|
|
|
|
**Permission:** owner یا ROLE_ADMIN
|
|
|
|
```json
|
|
{ "name": "رادیولوژی دیجیتال", "active": true }
|
|
```
|
|
|
|
---
|
|
|
|
## DELETE /api/v1/service-section/{uuid}
|
|
|
|
حذف بخش (cascade — همه ServiceItem های آن حذف میشوند).
|
|
|
|
**Permission:** owner
|
|
|
|
---
|
|
|
|
## GET /api/v1/service-items/{sectionUuid}
|
|
|
|
لیست سرویسهای یک بخش.
|
|
|
|
**Response 200:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"uuid": "...",
|
|
"section_uuid": "...",
|
|
"staff_uuid": "...",
|
|
"staff_name": "علی محمدی",
|
|
"staff": { "uuid": "...", "full_name": "علی محمدی" },
|
|
"staff_members": [
|
|
{ "uuid": "...", "full_name": "علی محمدی" },
|
|
{ "uuid": "...", "full_name": "سحر رحمانی" }
|
|
],
|
|
"name": "رادیوگرافی مستقیم",
|
|
"price_rials": 500000,
|
|
"active": true,
|
|
"insurance_covered": false,
|
|
"insurance_price_rials": null,
|
|
"duration_minutes": 50,
|
|
"created_at": 1718000000,
|
|
"updated_at": 1718000000
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## POST /api/v1/service-item
|
|
|
|
ایجاد سرویس جدید.
|
|
|
|
**Permission:** `IS_AUTHENTICATED_FULLY` + پنل Basic+
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"section_uuid": "...",
|
|
"name": "رادیوگرافی مستقیم",
|
|
"price_rials": 500000,
|
|
"staff_uuid": "...",
|
|
"insurance_covered": true,
|
|
"insurance_price_rials": 200000,
|
|
"duration_minutes": 50
|
|
}
|
|
```
|
|
|
|
| فیلد | نوع | الزامی |
|
|
|------|-----|--------|
|
|
| section_uuid | UUID | ✅ |
|
|
| name | string | ✅ |
|
|
| price_rials | integer | ❌ (پیشفرض 0) — «قیمت پایه» |
|
|
| staff_uuids | UUID[] | ❌ — پرسنل مسئول (چند نفر). ترجیح داده میشود |
|
|
| staff_uuid | UUID | ❌ — legacy تکپرسنل (اگر `staff_uuids` نباشد استفاده میشود) |
|
|
| insurance_covered | boolean | ❌ (پیشفرض false) — آیا خدمت شامل بیمه میشود |
|
|
| insurance_price_rials | integer\|null | ❌ — سهم/قیمت بیمار با بیمه |
|
|
| duration_minutes | integer\|null | ❌ — «زمان متوسط» انجام خدمت به دقیقه (`""`/`null` = بدون مقدار) |
|
|
|
|
**Response 201:** ServiceItem object (شامل `insurance_covered` و `insurance_price_rials`)
|
|
|
|
> **قیمت واحد:** هنگام ساخت سرویس، یک تعرفه برای **سال جاری** با همان `price_rials` بهصورت خودکار ثبت میشود. قیمت سرویس = تعرفهی سال جاری است و همهجا (صورتحساب، مراجعه، مطالبات) از همین قیمت استفاده میشود.
|
|
|
|
---
|
|
|
|
## PATCH /api/v1/service-item/{uuid}
|
|
|
|
ویرایش سرویس.
|
|
|
|
```json
|
|
{
|
|
"name": "رادیوگرافی دیجیتال",
|
|
"price_rials": 600000,
|
|
"staff_uuid": null,
|
|
"active": false,
|
|
"insurance_covered": true,
|
|
"insurance_price_rials": 250000,
|
|
"duration_minutes": 30
|
|
}
|
|
```
|
|
|
|
> فیلد `duration_minutes` (زمان متوسط، دقیقه) در پاسخِ `toArray` و در ساخت/ویرایش پشتیبانی میشود؛ `""`/`null` آن را پاک میکند.
|
|
|
|
> **پرسنل چندنفره:** یک سرویس میتواند چند پرسنل داشته باشد. `staff_uuids` (آرایه) ترجیح داده میشود؛ در نبود آن، `staff_uuid` تکنفره بهصورت backward-compatible پذیرفته میشود. پاسخ همیشه `staff_members[]` (کامل) و `staff`/`staff_uuid`/`staff_name` (نفر اول، برای سازگاری) را برمیگرداند. هر پرسنل باید متعلق به همان tenant (`entity_type`/`entity_id`) باشد؛ در غیر این صورت → `422 ERR_VALIDATION_001` (`field: staff_uuids`). همین قید روی `POST /service-item` نیز اعمال میشود.
|
|
|
|
---
|
|
|
|
## DELETE /api/v1/service-item/{uuid}
|
|
|
|
حذف سرویس.
|
|
|
|
اگر سرویس در پرونده بیماری استفاده شده باشد، خطا برمیگرداند:
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"errors": [{ "code": "ERR_SERVICE_ITEM_IN_USE", "message": "این سرویس در پرونده بیمار ثبت شده است" }]
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | HTTP | توضیح |
|
|
|------|------|-------|
|
|
| ERR_SUBSCRIPTION_REQUIRED | 403 | نیاز به پنل Basic+ |
|
|
| ERR_SERVICE_NOT_FOUND | 404 | سرویس یافت نشد |
|
|
| ERR_SERVICE_ITEM_IN_USE | 409 | سرویس در پرونده بیمار استفاده شده |
|
|
|
|
---
|
|
|
|
## تعرفهی نسخهدار سالانه (Tariff) — فاز ۳ سیستم صورتحساب
|
|
|
|
هر خدمت میتواند برای هر سال شمسی یک تعرفه داشته باشد. اگر تعرفهی سالی ثبت نشود، به `price_rials` خود خدمت fallback میشود (`TariffService::resolvePrice`). سال جاری شمسی سمت سرور با `IntlDateFormatter` (تقویم persian) محاسبه میشود.
|
|
|
|
### GET /api/v1/service-items/{uuid}/tariffs
|
|
|
|
لیست تعرفههای یک خدمت + قیمت پیشفرض + سال جاری.
|
|
|
|
**Permission:** `IS_AUTHENTICATED_FULLY` (مالک خدمت)
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"current_year": 1405,
|
|
"default_price_rials": 500000,
|
|
"data": [
|
|
{ "uuid": "…", "service_item_id": 12, "year": 1405, "price_rials": 600000, "is_active": true },
|
|
{ "uuid": "…", "service_item_id": 12, "year": 1404, "price_rials": 500000, "is_active": true }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### PUT /api/v1/service-items/{uuid}/tariffs/{year}
|
|
|
|
ثبت/بهروزرسانی تعرفهی یک سال (upsert). `year` بین ۱۳۹۰ تا ۱۵۰۰.
|
|
|
|
**Body:**
|
|
```json
|
|
{ "price_rials": 600000 }
|
|
```
|
|
|
|
**Response 200:** `{ success, data: { …tariff } }`
|
|
|
|
> اگر `year` برابر **سال جاری** باشد، `ServiceItem.price_rials` هم با همین مقدار همگام میشود (قیمت واحد). تعرفهی سالهای دیگر فقط برای محاسبهی صورتحساب همان سال (`TariffService::resolvePrice`) بهکار میرود و قیمت پایهی سرویس را تغییر نمیدهد. همچنین `PATCH /service-item/{uuid}` با تغییر `price_rials`، تعرفهی سال جاری را upsert میکند.
|
|
|
|
**Errors:**
|
|
| Code | HTTP | توضیح |
|
|
|------|------|-------|
|
|
| ERR_SERVICE_NOT_FOUND | 404 | سرویس یافت نشد |
|
|
| ERR_VALIDATION_001 | 422 | سال نامعتبر |
|