Files
onboard/app/Services/StatisticsService.php
2026-03-25 17:37:32 +09:00

379 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// app/Services/StatisticsService.php
namespace App\Services;
use App\Models\Department;
use App\Models\MetrikaResult;
use App\Models\Report;
use App\Models\User;
use App\Factories\MetricCalculatorFactory;
use App\Models\UserDepartment;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class StatisticsService
{
public function __construct(
protected BedDayService $bedDayService
)
{
}
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
$this->bedDayService->clearMemoryCache();
// Годовой план
$recipientPlanOfYear = 0;
$progressPlanOfYear = 0;
// 1. Получаем отделения
$departments = Department::select('department_id', 'name_short', 'rf_department_type', 'user_name', 'order')
->with('departmentType')
->join((new UserDepartment)->getTable(), (new Department)->getTable() . '.department_id', (new UserDepartment)->getTable() . '.rf_department_id')
->where((new UserDepartment)->getTable() . '.rf_user_id', $user->id)
->orderBy('rf_department_type')
->orderBy((new UserDepartment)->getTable() . '.order', 'asc')
->get()
->groupBy('departmentType.name_full');
if ($departments->isEmpty()) {
return $this->emptyResponse();
}
// Рассчитываем коэффициент периода (дни периода / 365)
$start = Carbon::parse($startDate);
$end = Carbon::parse($endDate);
$monthsInPeriod = $start->diffInMonths($end); // +1 чтобы включить оба дня
$periodCoefficient = $monthsInPeriod / 12;
$monthsInPeriod = ceil($start->diffInMonths($end));
// foreach ($departments as $departmentType) {
// foreach ($departmentType as $department) {
// if ($department->recipientPlanOfYear() === null) continue;
// $recipientPlanOfYear += (int)$department->recipientPlanOfYear()->value;
// }
// }
$allDeptIds = $departments->flatten()->pluck('department_id')->toArray();
// 2. Получаем ВСЕ метрики за период ОДНИМ запросом
$metrics = DB::table('reports as r')
->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id')
->whereIn('r.rf_department_id', $allDeptIds)
->whereDate('r.sent_at', '>=', $startDate)
->whereDate('r.sent_at', '<=', $endDate)
->select(
'r.rf_department_id',
'mr.rf_metrika_item_id',
DB::raw('SUM(CAST(mr.value AS DECIMAL)) as total'),
DB::raw('COUNT(*) as records_count')
)
->whereIn('mr.rf_metrika_item_id', [1, 4, 12, 11, 10, 13, 7, 9, 17, 14, 16, 18, 19, 22])
->groupBy('r.rf_department_id', 'mr.rf_metrika_item_id')
->get()
->groupBy('rf_department_id');
// 3. Получаем текущих пациентов
$currentPatients = DB::table('reports as r')
->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id')
->whereIn('r.rf_department_id', $allDeptIds)
->where('mr.rf_metrika_item_id', 8)
->where('r.sent_at', '<=', $endDate)
->select('r.rf_department_id', 'mr.value', 'r.created_at')
->orderBy('r.rf_department_id') // Сначала поле из DISTINCT ON
->orderBy('r.sent_at', 'desc') // Потом остальные
->distinct('r.rf_department_id')
->get()
->keyBy('rf_department_id');
// 4. Получаем количество коек
$beds = DB::table('department_metrika_defaults')
->whereIn('rf_department_id', $allDeptIds)
->where('rf_metrika_item_id', 1)
->select('rf_department_id', 'value')
->get()
->keyBy('rf_department_id');
// 5. Собираем данные
$groupedData = [];
$totalsByType = [];
$grandRecipientPlan = 0;
$grandProgressPlan = 0;
foreach ($departments as $typeName => $deptList) {
$groupedData[$typeName] = [];
$totalsByType[$typeName] = $this->initTypeTotals();
foreach ($deptList as $dept) {
$deptId = $dept->department_id;
$lastReport = Report::where('rf_department_id', $deptId)
->whereDate('sent_at', '>=', Carbon::parse($startDate)->format('Y-m-d'))
->whereDate('sent_at', '<=', Carbon::parse($endDate)->format('Y-m-d'))
->orderBy('sent_at', 'desc')
->first();
// Базовые показатели
$bedsCount = (int)($beds[$deptId]->value ?? 0);
$currentCount = (int)($currentPatients[$deptId]->value ?? 0);
// Получаем годовой план
$annualPlanModel = $dept->recipientPlanOfYear();
// $annualPlan = $annualPlanModel ? (int)$annualPlanModel->value : 0;
$annualPlan = $annualPlanModel ? (int)$annualPlanModel->value : 0;
$oneMonthPlan = ceil($annualPlan / 12);
// Рассчитываем план на период
$periodPlan = round($oneMonthPlan * $monthsInPeriod);
// $periodPlan = round($annualPlan * $periodCoefficient);
// Счетчики
$plan = 0;
$emergency = 0;
$planSurgical = 0;
$emergencySurgical = 0;
$transferred = 0;
$outcome = 0;
$deceased = 0;
$staff = 0;
$observable = 0;
$unwanted = 0;
$bedDaysSum = 0;
if (isset($metrics[$deptId])) {
foreach ($metrics[$deptId] as $item) {
$value = (float)$item->total;
match ($item->rf_metrika_item_id) {
4 => $plan = (int)$value,
12 => $emergency = (int)$value,
11 => $planSurgical = (int)$value,
10 => $emergencySurgical = (int)$value,
13 => $transferred = (int)$value,
7 => $outcome = (int)$value,
9 => $deceased = (int)$value,
17 => $staff = (int)$value,
14 => $observable = (int)$value,
16 => $unwanted = (int)$value,
18 => $bedDaysSum += $value,
19 => $lethalitySum = $value,
// 24 => $completePlanProgress = (int)$value,
default => null
};
}
}
$grandRecipientPlan += $periodPlan;
$grandProgressPlan += $outcome;
$percentPlanOfYear = $periodPlan > 0 ? round($outcome * 100 / $periodPlan) : 0;
// Расчеты
$allCount = $plan + $emergency;
$percentLoaded = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0;
// Средний койко-день
$avgBedDays = $outcome > 0 ? round($bedDaysSum / $outcome, 2) : 0;
// Предоперационный койко-день
$preoperativeValue = $lastReport
? (float)MetrikaResult::where('rf_report_id', $lastReport->report_id)
->where('rf_metrika_item_id', 21)
->value('value')
: 0;
// Летальность
$lethality = $outcome > 0 ? round(($deceased / $outcome) * 100, 2) : 0;
$departmentName = $dept->user_name ?? $dept->name_short;
$progressPlanOfYear += $outcome;
$data = [
'department' => $departmentName,
'department_id' => $deptId,
'beds' => $bedsCount,
'recipients' => [
'all' => $allCount,
'plan' => $plan,
'emergency' => $emergency,
'transferred' => $transferred,
],
'outcome' => $outcome,
'consist' => $currentCount,
'percentLoadedBeds' => $percentLoaded,
'surgical' => [
'plan' => $planSurgical,
'emergency' => $emergencySurgical
],
'deceased' => $deceased,
'countStaff' => $staff,
'countObservable' => $observable,
'countUnwanted' => $unwanted,
'averageBedDays' => $avgBedDays,
'preoperativeDays' => $preoperativeValue,
'progressPlanOfYear' => $periodPlan,
'percentPlanOfYear' => $percentPlanOfYear,
'lethality' => $lethality,
'type' => $typeName,
'isDepartment' => true,
'isReportToday' => $lastReport ? Carbon::parse($lastReport->sent_at)->isSameDay($endDate) : null,
];
$groupedData[$typeName][] = $data;
$this->updateTypeTotals($totalsByType[$typeName], $data);
}
}
return [
'data' => $this->buildFinalData($groupedData, $totalsByType),
'totalsByType' => $totalsByType,
'grandTotals' => $this->calculateGrandTotals($totalsByType),
'recipientPlanOfYear' => [
'plan' => $grandRecipientPlan, // Сумма планов по периоду
'progress' => $grandProgressPlan, // Сумма фактов по периоду
]
];
}
/**
* Инициализация итогов по типу
*/
private function initTypeTotals(): array
{
return [
'departments_count' => 0,
'beds_sum' => 0,
'recipients_all_sum' => 0,
'recipients_plan_sum' => 0,
'recipients_emergency_sum' => 0,
'recipients_transferred_sum' => 0,
'outcome_sum' => 0,
'consist_sum' => 0,
'plan_surgical_sum' => 0,
'emergency_surgical_sum' => 0,
'deceased_sum' => 0,
'percentLoadedBeds_total' => 0,
'percentLoadedBeds_count' => 0,
'staff_sum' => 0,
'observable_sum' => 0,
'unwanted_sum' => 0,
];
}
/**
* Обновление итогов по типу
*/
private function updateTypeTotals(array &$totals, array $data): void
{
$totals['departments_count']++;
$totals['beds_sum'] += $data['beds'];
$totals['recipients_all_sum'] += $data['recipients']['all'];
$totals['recipients_plan_sum'] += $data['recipients']['plan'];
$totals['recipients_emergency_sum'] += $data['recipients']['emergency'];
$totals['recipients_transferred_sum'] += $data['recipients']['transferred'];
$totals['outcome_sum'] += $data['outcome'];
$totals['consist_sum'] += $data['consist'];
$totals['plan_surgical_sum'] += $data['surgical']['plan'];
$totals['emergency_surgical_sum'] += $data['surgical']['emergency'];
$totals['deceased_sum'] += $data['deceased'];
$totals['percentLoadedBeds_total'] += $data['percentLoadedBeds'];
$totals['percentLoadedBeds_count']++;
$totals['staff_sum'] += $data['countStaff'];
$totals['observable_sum'] += $data['countObservable'];
$totals['unwanted_sum'] += $data['countUnwanted'];
}
/**
* Расчет общих итогов
*/
private function calculateGrandTotals(array $totalsByType): array
{
$grand = $this->initTypeTotals();
foreach ($totalsByType as $totals) {
foreach ($grand as $key => &$value) {
if (isset($totals[$key])) {
$value += $totals[$key];
}
}
}
return $grand;
}
/**
* Построение финальных данных
*/
private function buildFinalData(array $groupedData, array $totalsByType): array
{
$final = [];
foreach ($groupedData as $type => $items) {
$final[] = [
'isGroupHeader' => true,
'groupName' => $type,
'colspan' => 16
];
foreach ($items as $item) {
$final[] = $item;
}
if (!empty($items) && isset($totalsByType[$type])) {
$final[] = $this->createTotalRow($type, $totalsByType[$type], false);
}
}
return $final;
}
/**
* Создание строки итогов
*/
private function createTotalRow(string $type, array $total, bool $isGrandTotal): array
{
return [
'isTotalRow' => !$isGrandTotal,
'isGrandTotal' => $isGrandTotal,
'department' => $isGrandTotal ? 'ОБЩИЕ ИТОГИ:' : 'ИТОГО:',
'beds' => '—',
'recipients' => [
'all' => $total['recipients_all_sum'],
'plan' => $total['recipients_plan_sum'],
'emergency' => $total['recipients_emergency_sum'],
'transferred' => $total['recipients_transferred_sum'],
],
'outcome' => $total['outcome_sum'],
'consist' => $total['consist_sum'],
'percentLoadedBeds' => '—',
'surgical' => [
'plan' => $total['plan_surgical_sum'],
'emergency' => $total['emergency_surgical_sum']
],
'deceased' => $total['deceased_sum'],
'averageBedDays' => '—',
'preoperativeDays' => '—',
'lethality' => '—',
'type' => $type,
'departments_count' => $total['departments_count'],
'countStaff' => $total['staff_sum'],
'countObservable' => $total['observable_sum'],
'countUnwanted' => $total['unwanted_sum'],
'isBold' => true
];
}
/**
* Пустой ответ
*/
private function emptyResponse(): array
{
return [
'data' => [],
'totalsByType' => [],
'grandTotals' => $this->initTypeTotals()
];
}
}