feat(catalog): manage global categories from settings
Categories are the taxonomy both services and resources select from, but
until now they could only be reached through the service catalog, so every
environment ended up with its own spelling of "whole body".
- Settings > Categories page: global CRUD plus the "includes" edge
- POST/GET/DELETE /api/v1/service-category/{uuid}/includes — a DAG, kept
separate from `parent` because "hand" sits under both "whole body" and
"upper limb"; a cycle is refused with 422
- PUT /api/v1/resource/{uuid}/categories — full replacement, and a category
from another environment is rejected explicitly since the uuid arrives in
the request body where TenantFilter does not reach
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -499,6 +499,62 @@ override فقط وقتی اعمال میشود که `branch_uuid` به `valid
|
||||
است (`outpatient`/`inpatient`) که روی خودِ `ServiceItem` هم نشسته. با `ServiceSection` هم
|
||||
فرق دارد — آن «بخش کلینیک» است، این تاکسونومی کاتالوگ. عمق حداکثر ۴ سطح.
|
||||
|
||||
دسته **سراسری** است: یک بار در «تنظیمات ← دستهبندیها» (`/admin/service-categories`) ساخته
|
||||
میشود و سرویس و منبع فقط از همان انتخاب میکنند. صفحهٔ سرویس و منبع عمداً امکان ساخت دسته
|
||||
ندارند، وگرنه هر کاربر «تمام بدن» خودش را با املای خودش میسازد.
|
||||
|
||||
### یال «شامل بودن» — `CatalogCategoryInclude`
|
||||
|
||||
جدا از `parent`. `parent` سلسلهمراتب نمایشی است و هر دسته فقط یک والد دارد؛ یال شاملبودن یک
|
||||
**DAG** است، چون «دست» هم زیر «تمام بدن» است و هم زیر «اندام فوقانی». موتور انتخاب سرویس از
|
||||
همین یالها استفاده میکند تا رزرو همزمان «لیزر تمام بدن» و «لیزر دست» را رد کند.
|
||||
|
||||
| متد | مسیر | مجوز |
|
||||
|---|---|---|
|
||||
| GET | `/api/v1/service-category/{uuid}/includes` | `appointment_settings.view` |
|
||||
| POST | `/api/v1/service-category/{uuid}/includes` | `appointment_settings.update` |
|
||||
| DELETE | `/api/v1/service-category/{uuid}/includes/{childUuid}` | `appointment_settings.update` |
|
||||
|
||||
**POST body:** `{ "child_category_uuid": "<uuid>" }` — الزامی.
|
||||
|
||||
```jsonc
|
||||
// POST /api/v1/service-category/{whole}/includes → 201 (تکرار همان یال → 200، نه خطا)
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "f06c1e09-00d8-4f6f-b63b-c41efd5291f1",
|
||||
"parent_uuid": null,
|
||||
"name": "دست (doc)",
|
||||
"sort_order": 0,
|
||||
"active": true,
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
|
||||
// GET → آرایهای از همان شکل
|
||||
// DELETE → { "success": true, "data": null }
|
||||
```
|
||||
|
||||
حلقه رد میشود — «تمام بدن شامل دست» و بعد «دست شامل تمام بدن» → **422**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"errors": [{
|
||||
"code": "ERR_VALIDATION_001",
|
||||
"message": "«دست (doc)» از قبل زیرمجموعهٔ «تمام بدن (doc)» است؛ این دو نمیتوانند شامل هم باشند",
|
||||
"field": "child_category_uuid"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
خطاها: `404` دستهٔ نامعتبر · `422` نبودِ `child_category_uuid`، حلقه، یا دستهٔ محیط دیگر.
|
||||
|
||||
### دستهٔ منبع — `PUT /api/v1/resource/{uuid}/categories`
|
||||
|
||||
مستند کامل در [`resource.md`](resource.md).
|
||||
|
||||
## تستها
|
||||
|
||||
```bash
|
||||
|
||||
@@ -335,6 +335,39 @@
|
||||
> **اتمی است.** اعتبارسنجی کل فهرست پیش از هر حذفی انجام میشود، پس یک ردیف نامعتبر
|
||||
> در انتهای فهرست، مهارتهای درستِ قبلی را پاک نمیکند و بعد ۴۲۲ برگرداند.
|
||||
|
||||
### `PUT /api/v1/resource/{uuid}/categories`
|
||||
|
||||
مجوز: `appointment_settings.update`.
|
||||
|
||||
دستهٔ منبع از **کاتالوگ سراسری** انتخاب میشود (`CatalogCategory`) — همان دستههایی که سرویس
|
||||
هم از آنها استفاده میکند. ساخت دسته اینجا ممکن نیست؛ فقط در «تنظیمات ← دستهبندیها»
|
||||
([`clinic-services.md`](clinic-services.md)).
|
||||
|
||||
جایگزینی **کامل**، مثل `skills`: `{"category_uuids":[]}` همه را پاک میکند.
|
||||
|
||||
```json
|
||||
{ "category_uuids": ["8ae755b5-5f27-404e-9b63-1b29693a9039"] }
|
||||
```
|
||||
|
||||
پاسخ ۲۰۰ کلِ منبع است؛ بخش `categories` آن (خروجی واقعی):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "ce070910-7038-4f50-9f7a-1b35ec1a67f7",
|
||||
"name": "اتاق ۱",
|
||||
"categories": [
|
||||
{ "uuid": "8ae755b5-5f27-404e-9b63-1b29693a9039", "name": "دست (doc)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**۴۲۲:** نبودِ `category_uuids` (`{"code":"ERR_VALIDATION_002","message":"فیلد category_uuids الزامی است","field":"category_uuids"}`)
|
||||
· دستهٔ محیط دیگر. uuid از بدنهٔ درخواست میآید و `TenantFilter` رویش اعمال نمیشود، پس محیطِ
|
||||
هر دسته صریحاً با محیط منبع مقایسه میشود.
|
||||
|
||||
---
|
||||
|
||||
## استخر منابع
|
||||
|
||||
Reference in New Issue
Block a user