Files
clinicpro/docs/api/reports.md
T
hamedandClaude Opus 5 3c43955800 feat(events): domain event outbox and the two reports that close the loop
Tasks 07 through 13 each changed something the rest of the system might want
to know about, with no contract for saying so. And task 05 shipped a powerful
segment editor with no feedback on whether a clinic defined its segments right.

Events
- A closed list of names, because a consumer branches on the string and a
  one-letter typo would produce an event nobody hears and no error either
- Payloads carry uuids and scalars only; non-scalars are dropped, not
  serialised, so a consumer always fetches fresh rather than reading a stale
  detached entity
- record() deliberately does not flush: the event row commits with the change
  it describes, so a rolled-back transaction leaves no event behind. A test
  pins exactly that
- app:events:publish drains the outbox; five failed attempts park a row with
  its error rather than deleting it, because a silently dropped event is a
  loss with no trace. app:events:prune only ever removes published rows

Reports
- Resource utilisation separates available, occupied and active minutes.
  The gap between occupied and active is what exposes a bad segment
  definition, and available is multiplied by capacity so a three-chair room
  does not read as permanently over 100%
- A resource with no calendar reports utilization: null, not zero — dividing
  by zero means something different from being idle
- Plan accuracy compares planned against actual duration per service and
  flags both directions: running short wastes capacity that could have been
  sold. Its row links straight to editing that service's segments, because a
  report with no route to a fix does not get read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:27:54 +03:30

4.7 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   # ۱۶ تست