Files
clinicpro/docs/api/reports.md
T
hamedandClaude Opus 5 b3c331f0cb perf(reports): read every resource's calendar in one batch, and close the owed tests
Writing the query-count test that task 14 owed showed the growth was real: one
resource cost 10 queries, six cost 33 — about five per resource, because the
available-minutes figure walked each resource's calendar on its own.

Holidays, tenant overrides and branch hours are identical for every resource in
a report, so they now load once outside the loop; shifts and exceptions load for
all resources in one query each. The batched path is a new method rather than a
change to rawAvailability, which the booking engine also calls. The test pins
the shape of the growth, not an exact count.

Also landed:

- app:segment:seed-templates with beauty, dental and physio presets. Building
  four segments and their requirements by hand is the first thing a new clinic
  must do and the most tedious; this gives them something to edit instead of an
  empty page. It refuses to touch a service that already has segments unless
  --force, and it will not invent resource types the tenant never defined.
- book-all is all-or-nothing, proven rather than asserted: with a calendar open
  one day a week and a 1-2 day protocol gap, session one finds a slot and
  session two cannot, and every session must come back planned.
- credit_refundable: false takes the credit back with a negative adjustment and
  deletes nothing — the ledger stays append-only.
- the segments editor has frontend tests, including that it sends back what the
  user sees and renders read-only without the permission.

useBranches now returns [] for a non-array payload instead of throwing
"branches.map is not a function" and taking the page down with it.

BookingLocationsScanTest built a Clinic around a Doctor loaded from a different
manager, which Doctrine treats as a new entity; it flushed fine most runs and
failed on cascade in others. It now loads the doctor from the same manager.

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

5.5 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 همین را نگه می‌دارد — عددِ دقیق را پین نمی‌کند، فقط رشدِ خطی را رد می‌کند.