Files
onboard/app/Services/StatisticsService.php

441 lines
18 KiB
PHP
Raw 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\ReportDuty;
use App\Models\User;
use App\Models\UserDepartment;
use App\Services\MetricCalculators\PlanCalculator;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class StatisticsService
{
public function __construct(
protected BedDayService $bedDayService,
protected PlanCalculator $planCalculator,
) {}
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
$this->bedDayService->clearMemoryCache();
// 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();
}
$allDeptIds = $departments->flatten()->pluck('department_id')->toArray();
// 2а. Нежелательные события по отделениям за период (прямой запрос)
$unwantedCounts = DB::table('duty_unwanted_events as due')
->join('report_duties as rd', 'rd.id', '=', 'due.report_duty_id')
->whereIn('rd.rf_department_id', $allDeptIds)
->where('rd.period_start', '>=', $startDate)
->where('rd.period_end', '<=', $endDate)
->select('rd.rf_department_id', DB::raw('COUNT(*) as count'))
->groupBy('rd.rf_department_id')
->get()
->keyBy('rf_department_id');
// 2б. Пациенты на контроле по отделениям за период (прямой запрос)
$observableCounts = DB::table('observable_medical_histories as omh')
->join('report_duty_patients as rdp', 'rdp.original_id', '=', 'omh.original_id')
->join('report_duties as rd', 'rd.id', '=', 'rdp.report_duty_id')
->whereIn('rd.rf_department_id', $allDeptIds)
->where('omh.observable_in', '>=', $startDate)
->where('omh.observable_in', '<=', $endDate)
->select('rd.rf_department_id', DB::raw('COUNT(DISTINCT omh.id) as count'))
->groupBy('rd.rf_department_id')
->get()
->keyBy('rf_department_id');
// 2. Получаем ВСЕ метрики за период ОДНИМ запросом
$metrics = DB::table('report_duties as r')
->join('duty_report_metric_results as mr', 'r.id', '=', 'mr.rf_report_id')
->whereIn('r.rf_department_id', $allDeptIds)
->where('r.period_start', '>=', $startDate)
->where('r.period_end', '<=', $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')
)
->groupBy('r.rf_department_id', 'mr.rf_metrika_item_id')
->get()
->groupBy('rf_department_id');
// 3. Получаем текущих пациентов
$currentPatients = DB::table('report_duties as r')
->join('duty_report_metric_results as mr', 'r.id', '=', 'mr.rf_report_id')
->whereIn('r.rf_department_id', $allDeptIds)
->where('mr.rf_metrika_item_id', 8)
->where('r.period_end', '<=', $endDate)
->select('r.rf_department_id', 'mr.value', 'r.created_at')
->orderBy('r.rf_department_id') // Сначала поле из DISTINCT ON
->orderBy('r.period_end', '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;
$departmentsPlans = $this->planCalculator->calculate($allDeptIds, $startDate, $endDate);
foreach ($departments as $typeName => $deptList) {
$groupedData[$typeName] = [];
$totalsByType[$typeName] = $this->initTypeTotals();
foreach ($deptList as $dept) {
$deptId = $dept->department_id;
$lastReportQuery = ReportDuty::where('rf_department_id', $deptId);
if ($isRangeOneDay) {
$lastReportQuery->exactPeriod($startDate, $endDate);
} else {
$lastReportQuery->withinPeriod($startDate, $endDate);
}
$lastReport = $lastReportQuery
->onlySubmitted()
->orderBy('period_end', 'desc')
->first();
// Базовые показатели
$bedsCount = (int) ($beds[$deptId]->value ?? 0);
$currentCount = (int) ($currentPatients[$deptId]->value ?? 0);
// Счетчики
$plan = 0;
$emergency = 0;
$planSurgical = 0;
$emergencySurgical = 0;
$transferred = 0;
$outcome = 0;
$deceased = 0;
$staff = 0;
$observable = (int) ($observableCounts[$deptId]->count ?? 0);
$unwanted = (int) ($unwantedCounts[$deptId]->count ?? 0);
$bedDaysSum = 0;
$avgBedDays = 0;
$preoperativeSum = 0;
$preoperativePatientCount = 0;
$preoperativeTotalRecords = 0;
$preoperativePatientRecords = 0;
$preoperativeAverageSum = 0;
$preoperativeAverageCount = 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,
25 => $bedDaysSum += $value,
19 => $lethalitySum = $value,
21 => $preoperativeAverageSum += $value,
26 => $preoperativeSum += $value,
27 => $preoperativePatientCount += (int) $value,
// 24 => $completePlanProgress = (int)$value,
default => null
};
if ((int) $item->rf_metrika_item_id === 21) {
$preoperativeAverageCount += (int) $item->records_count;
}
if ((int) $item->rf_metrika_item_id === 26) {
$preoperativeTotalRecords += (int) $item->records_count;
}
if ((int) $item->rf_metrika_item_id === 27) {
$preoperativePatientRecords += (int) $item->records_count;
}
}
}
// Расчеты
$allCount = $plan + $emergency;
$percentLoaded = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0;
// Средний койко-день
$avgBedDays = $outcome > 0 ? round($bedDaysSum / $outcome, 1) : 0;
// Предоперационный койко-день
$canUsePreoperativeTotals = $preoperativePatientCount > 0
&& $preoperativeTotalRecords > 0
&& $preoperativePatientRecords >= $preoperativeTotalRecords;
$preoperativeValue = $canUsePreoperativeTotals
? round($preoperativeSum / $preoperativePatientCount, 1)
: ($preoperativeAverageCount > 0 ? round($preoperativeAverageSum / $preoperativeAverageCount, 1) : 0);
// Летальность
$lethality = $outcome > 0 ? round(($deceased / $outcome) * 100, 2) : 0;
$departmentName = $dept->user_name ?? $dept->name_short;
$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,
'bedDaysSum' => $bedDaysSum,
'preoperativeDays' => $preoperativeValue,
'preoperativeSum' => $preoperativeSum,
'preoperativePatientCount' => $preoperativePatientCount,
'plan' => $departmentsPlans[$deptId],
// 'progressPlanOfYear' => $cumulativePlan,
// 'percentPlanOfYear' => $percentPlanOfYear,
// 'needPlanOfYear' => $cumulativePlan > 0 && $cumulativePlan > $outcome
// ? $cumulativePlan - $outcome
// : 0,
// Плановые показатели
// 'cumulative_plan' => $cumulativePlan, // План с начала года нарастающим (включая текущий месяц)
// 'current_month_plan_only' => $currentMonthPlanOnly, // План только на текущий месяц
// 'debt_from_year_start' => $debtFromYearStart, // Долг с начала года (невыполненный план за прошлые месяцы)
// 'total_debt' => $totalDebt, // ИТОГО долг = план на текущий месяц + долг с начала года
// 'currentMonthDebt' => $currentMonthDebt, // Выписать по плану в текущем месяце
// Фактические показатели
// 'actual_year_to_date' => $actualToDate, // Факт с начала года (включая текущий месяц)
// Процент выполнения
// 'cumulative_percent' => $cumulativePlan > 0 ? round($actualToDate * 100 / $cumulativePlan, 2) : 0,
'lethality' => $lethality,
'type' => $typeName,
'isDepartment' => true,
'isReportToday' => $lastReport ? Carbon::parse($lastReport->period_end)->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,
'bedDaysSum' => 0,
'preoperativeSum' => 0,
'preoperativePatientCount' => 0,
'percentLoaded' => 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['bedDaysSum'] += $data['bedDaysSum'];
$totals['preoperativeSum'] += $data['preoperativeSum'];
$totals['preoperativePatientCount'] += $data['preoperativePatientCount'];
$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
{
$outcomeSum = $total['outcome_sum'];
$preopPatients = $total['preoperativePatientCount'];
$consistSum = $total['consist_sum'] ?? 0;
$bedsSum = $total['beds_sum'] ?? 0;
return [
'isTotalRow' => ! $isGrandTotal,
'isGrandTotal' => $isGrandTotal,
'department' => $isGrandTotal ? 'ОБЩИЕ ИТОГИ:' : 'ИТОГО:',
'beds' => $bedsSum,
'recipients' => [
'all' => $total['recipients_all_sum'],
'plan' => $total['recipients_plan_sum'],
'emergency' => $total['recipients_emergency_sum'],
'transferred' => $total['recipients_transferred_sum'],
],
'outcome' => $outcomeSum,
'consist' => $consistSum,
'percentLoadedBeds' => $consistSum > 0
? round(($consistSum / $bedsSum) * 100)
: 0, // '—',
'surgical' => [
'plan' => $total['plan_surgical_sum'],
'emergency' => $total['emergency_surgical_sum'],
],
'deceased' => $total['deceased_sum'],
'averageBedDays' => $outcomeSum > 0
? round($total['bedDaysSum'] / $outcomeSum, 1)
: 0,
'preoperativeDays' => $preopPatients > 0
? round($total['preoperativeSum'] / $preopPatients, 1)
: 0,
'lethality' => $outcomeSum > 0
? round(($total['deceased_sum'] / $outcomeSum) * 100, 2)
: 0,
'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(),
];
}
}