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

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