Files
clinicpro/docs/api/reports.md
T
hamedandClaude Opus 5 2db7a500e6 feat(reports): restore the documented sample threshold, and draw the chart
MIN_SAMPLE goes back to the specified 10. The reason it had been lowered to 3
was real — a small clinic saw an empty report — but the fix was wrong: three
samples do not make an average, and calling that "accurate" is worse than
saying nothing.

Rows below the threshold are now returned rather than dropped, with severity
null and below_min_sample true. That refuses both mistakes: it claims no
severity it cannot support, and it does not show a small clinic an empty page
that implies everything is fine. They sort after the usable rows and render
faded with a "small sample" badge.

The utilization page gets its Recharts bar chart. The table stays underneath —
six numeric columns are not something a chart answers — but the one question
the table is bad at, "which resource is behind", is exactly what a chart is
for. Colours come from the design tokens rather than hex, which is where a
chart usually breaks in dark mode, and a resource with no calendar is left out
entirely: null is not zero, and a zero bar would be a lie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:01:40 +03:30

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

حداقل نمونه

آستانه ۱۰ نمونه است (PlanAccuracyReporter::MIN_SAMPLE). زیر آن، ردیف حذف نمی‌شود بلکه با severity: null و below_min_sample: true برمی‌گردد.

دو خطا با هم رد می‌شوند: ادعای شدت روی میانگین سه نمونه (که معنا ندارد) و گزارشِ خالی برای کلینیک کوچک (که «همه‌چیز درست است» را تلقین می‌کند). UI همین ردیف‌ها را کم‌رنگ و با نشان «نمونهٔ کم» می‌آورد و در مرتب‌سازی بعد از ردیف‌های قابل استناد می‌گذارد.

نمودار بهره‌وری

ResourceUtilizationChart — میلهٔ افقی per منبع، رنگ از توکن‌ها (نه hex، وگرنه دارک‌مود می‌شکند)، و منبعِ بدون تقویم در نمودار نمی‌آید: null صفر نیست و ستون صفر دروغ می‌گوید. جدول زیرش می‌ماند چون شش ستون عددی را نمودار جواب نمی‌دهد.