# Reports — بهره‌وری منابع و دقت برنامه اندپوینت‌های `src/Report/*`. بند ۱۷ مستند، ریسک سوم: «کلینیک بخش‌های نوبت را اشتباه تعریف کند → ظرفیت غلط حساب می‌شود». این دو گزارش تنها بازخوردی‌اند که آن اشتباه را نشان می‌دهند. --- ## GET `/api/v1/reports/resource-utilization` | Query | Type | Required | Description | |---|---|---|---| | `branch_uuid` | string | ✅ | | | `from` / `to` | int | — | Unix؛ پیش‌فرض هفتهٔ گذشته، حداکثر ۹۰ روز | ### Response `200` ```json { "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` ```json { "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 | پیش‌فرض ۱۰۰، سقف ۵۰۰ | ```json { "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](../architecture/domain-events.md) --- ## خطاها | Code | HTTP | Description | |---|---|---| | `ERR_VALIDATION_001` | 422 | بازهٔ وارونه یا بزرگ‌تر از ۹۰ روز | | `ERR_VALIDATION_002` | 422 | `branch_uuid` غایب | ## تست‌ها ```bash 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` صفر نیست و ستون صفر دروغ می‌گوید. جدول زیرش می‌ماند چون شش ستون عددی را نمودار جواب نمی‌دهد.