Five places where the data existed and the screen did not use it. Booking a whole course had no button because it needs a doctor and the course does not carry one — each session can be with a different doctor. The page now asks for the doctor the same way the resource booking page does, and the button explains that it is all-or-nothing before it is pressed. A course whose package does not cover the remaining sessions is still valid — the rest is simply charged normally — but nobody was told. The course response carries package_balance and the shortfall, and the page warns. Before session six, not during it. The credit ledger already returned who recorded a row and which appointment it belonged to, and showed neither. An adjustable ledger without the name of the person who adjusted it is half an audit trail. Version history printed a JSON blob of each version's effects, which does not answer the question anyone actually has: what changed? It now diffs each version against the previous one, field by field, and says so plainly when a version changed nothing meaningful. A resource with no calendar showed "—" for utilization. Null means undefined, not zero, and the next step is always the same: set up the calendar. It is a link now. The report range also accepts a custom from/to, kept in the URL like the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
168 lines
6.1 KiB
Markdown
168 lines
6.1 KiB
Markdown
# Reports — بهرهوری منابع و دقت برنامه
|
|
|
|
اندپوینتهای `src/Report/*`. بند ۱۷ مستند، ریسک سوم: «کلینیک بخشهای نوبت را اشتباه
|
|
تعریف کند → ظرفیت غلط حساب میشود». این دو گزارش تنها بازخوردیاند که آن اشتباه را
|
|
نشان میدهند.
|
|
|
|
---
|
|
|
|
## GET `/api/v1/reports/resource-utilization`
|
|
|
|
| Query | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `branch_uuid` | string | ✅ | |
|
|
| `from` / `to` | int | — | Unix؛ پیشفرض هفتهٔ گذشته، حداکثر ۹۰ روز |
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"from": 1784880000,
|
|
"to": 1785484800,
|
|
"rows": [
|
|
{
|
|
"resource_uuid": "…",
|
|
"resource_name": "اپراتور مریم",
|
|
"role": "operator",
|
|
"available_minutes": 2400,
|
|
"occupied_minutes": 1800,
|
|
"active_minutes": 1200,
|
|
"utilization": 0.75,
|
|
"active_ratio": 0.5,
|
|
"wasted_capacity": false
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
سه عدد، سه معنا:
|
|
|
|
| عدد | یعنی |
|
|
|---|---|
|
|
| `available_minutes` | منبع طبق تقویمش چقدر در دسترس بوده |
|
|
| `occupied_minutes` | چقدر **گرفته** شده — شامل آمادهسازی، تمیزکاری و بخشهای انتظار |
|
|
| `active_minutes` | چقدر واقعاً کار شده — فقط بخشهایی که بیمار حاضر بوده |
|
|
|
|
فاصلهٔ `occupied` و `active` همان چیزی است که تعریف غلط بخشها را لو میدهد. `active_ratio`
|
|
زیر ۰٫۳ با `wasted_capacity: true` میآید: منبعی که هشت ساعت اشغال بوده ولی دو ساعت کار
|
|
کرده یا بخشهای `passive` زیادی گرفته یا انتظارها اشتباه به او نسبت داده شده.
|
|
|
|
⚠️ منبعی بدون تقویم `available_minutes: 0` و **`utilization: null`** میدهد، نه صفر:
|
|
تقسیم بر صفر معنای متفاوتی دارد — بهرهوریاش تعریفنشده است، نه بد.
|
|
|
|
ردیفهای `released` (لغوشده) در محاسبه نمیآیند، وگرنه هر لغو بهرهوری را بالا میبرد.
|
|
|
|
---
|
|
|
|
## GET `/api/v1/reports/plan-accuracy`
|
|
|
|
| Query | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `from` / `to` | int | — | پیشفرض هفتهٔ گذشته، حداکثر ۹۰ روز |
|
|
|
|
### Response `200`
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"from": 1784880000,
|
|
"to": 1785484800,
|
|
"rows": [
|
|
{
|
|
"service_uuid": "…",
|
|
"service_name": "لیزر فولبادی",
|
|
"sample_size": 4,
|
|
"planned_minutes": 60,
|
|
"actual_minutes": 90,
|
|
"deviation_percent": 50,
|
|
"severity": "high"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
| قدر مطلق انحراف | شدت |
|
|
|---|---|
|
|
| ≥ ۳۰٪ | `high` |
|
|
| ≥ ۱۵٪ | `medium` |
|
|
| ≥ ۵٪ | `low` |
|
|
| کمتر | `none` |
|
|
|
|
شدت از **قدر مطلق** میآید: سرویسی که نصف زمان پیشبینیشده طول میکشد هم غلط تعریف
|
|
شده — ظرفیتی که میشد فروخت، خالی مانده.
|
|
|
|
فقط نوبتهای `completed` شمرده میشوند (لغوشده چیزی دربارهٔ مدت واقعی نمیگوید) و
|
|
سرویس با کمتر از **سه** نمونه اصلاً نمیآید — میانگین دو نوبت، میانگین نیست.
|
|
|
|
مبنای «واقعی» فاصلهٔ ثبتشدهٔ اسلات است، نه ساعت ورود و خروج بیمار؛ آن دومی جایی ثبت
|
|
نمیشود و حدس زدنش بدتر از نداشتنش است.
|
|
|
|
---
|
|
|
|
## GET `/api/v1/domain-events`
|
|
|
|
**Permission:** `ROLE_ADMIN` (بقیه `403`)
|
|
|
|
| Query | Type | Description |
|
|
|---|---|---|
|
|
| `name` | string | فیلتر نام رویداد |
|
|
| `limit` | int | پیشفرض ۱۰۰، سقف ۵۰۰ |
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"uuid": "…",
|
|
"name": "AppointmentBooked",
|
|
"payload": { "appointment_uuid": "…", "hold_uuid": "…" },
|
|
"occurred_at": 1785484800,
|
|
"published_at": 1785484802,
|
|
"attempts": 0,
|
|
"last_error": null
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`published_at: null` یعنی هنوز در صندوق خروجی است. جزئیات:
|
|
[../architecture/domain-events.md](../architecture/domain-events.md)
|
|
|
|
---
|
|
|
|
## خطاها
|
|
|
|
| Code | HTTP | Description |
|
|
|---|---|---|
|
|
| `ERR_VALIDATION_001` | 422 | بازهٔ وارونه یا بزرگتر از ۹۰ روز |
|
|
| `ERR_VALIDATION_002` | 422 | `branch_uuid` غایب |
|
|
|
|
## تستها
|
|
|
|
```bash
|
|
ddev exec php bin/phpunit tests/Report # ۱۶ تست
|
|
```
|
|
|
|
## هزینهٔ کوئری
|
|
|
|
`occupied` و `active` هرکدام یک کوئری `GROUP BY` اند، و تقویم همهٔ منابع هم دستهای خوانده
|
|
میشود (`ResourceAvailabilityService::rawAvailabilityForAll`): تعطیلات و استثناهای محیط و
|
|
ساعت شعبه برای همهٔ منابع یکیاند و بیرون حلقه میآیند، شیفت و استثنای هر منبع هم با یک
|
|
کوئری برای همه.
|
|
|
|
پیش از این هر منبع پنج کوئری اضافه میآورد و گزارشِ یک کلینیک چهلمنبعی دویست کوئری
|
|
میشد. تست `testQueryCountDoesNotGrowWithTheNumberOfResources` همین را نگه میدارد —
|
|
عددِ دقیق را پین نمیکند، فقط رشدِ خطی را رد میکند.
|
|
|
|
## `utilization = null` در UI
|
|
|
|
بهجای «۰٪»، لینک **«تنظیم تقویم»** به تقویم همان منبع نمایش داده میشود. `null` یعنی
|
|
تعریفنشده نه صفر، و کار بعدی کاربر همان ساختن تقویم است — عددِ تنها او را به آنجا
|
|
نمیرساند.
|
|
|
|
بازهٔ گزارش علاوه بر هفته/ماه/سهماه، حالت **دلخواه** هم دارد (`from`/`to` شمسی در URL)
|
|
برای وقتی که کاربر دقیقاً میداند چه بازهای میخواهد.
|