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>
6.1 KiB
Reports — بهرهوری منابع و دقت برنامه
اندپوینتهای src/Report/*. بند ۱۷ مستند، ریسک سوم: «کلینیک بخشهای نوبت را اشتباه
تعریف کند → ظرفیت غلط حساب میشود». این دو گزارش تنها بازخوردیاند که آن اشتباه را
نشان میدهند.
GET /api/v1/reports/resource-utilization
| Query | Type | Required | Description |
|---|---|---|---|
branch_uuid |
string | ✅ | |
from / to |
int | — | Unix؛ پیشفرض هفتهٔ گذشته، حداکثر ۹۰ روز |
Response 200
{
"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
{
"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 | پیشفرض ۱۰۰، سقف ۵۰۰ |
{
"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
خطاها
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_001 |
422 | بازهٔ وارونه یا بزرگتر از ۹۰ روز |
ERR_VALIDATION_002 |
422 | branch_uuid غایب |
تستها
ddev exec php bin/phpunit tests/Report # ۱۶ تست
هزینهٔ کوئری
occupied و active هرکدام یک کوئری GROUP BY اند، و تقویم همهٔ منابع هم دستهای خوانده
میشود (ResourceAvailabilityService::rawAvailabilityForAll): تعطیلات و استثناهای محیط و
ساعت شعبه برای همهٔ منابع یکیاند و بیرون حلقه میآیند، شیفت و استثنای هر منبع هم با یک
کوئری برای همه.
پیش از این هر منبع پنج کوئری اضافه میآورد و گزارشِ یک کلینیک چهلمنبعی دویست کوئری
میشد. تست testQueryCountDoesNotGrowWithTheNumberOfResources همین را نگه میدارد —
عددِ دقیق را پین نمیکند، فقط رشدِ خطی را رد میکند.
utilization = null در UI
بهجای «۰٪»، لینک «تنظیم تقویم» به تقویم همان منبع نمایش داده میشود. null یعنی
تعریفنشده نه صفر، و کار بعدی کاربر همان ساختن تقویم است — عددِ تنها او را به آنجا
نمیرساند.
بازهٔ گزارش علاوه بر هفته/ماه/سهماه، حالت دلخواه هم دارد (from/to شمسی در URL)
برای وقتی که کاربر دقیقاً میداند چه بازهای میخواهد.