Files
clinicpro/docs/api/reports.md
T
hamedandClaude Opus 5 5c754244f2 feat(admin): finish the screens that were stopping one step short
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>
2026-08-01 15:07:27 +03:30

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)
برای وقتی که کاربر دقیقاً می‌داند چه بازه‌ای می‌خواهد.