379 lines
15 KiB
PHP
379 lines
15 KiB
PHP
<?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()
|
||
];
|
||
}
|
||
}
|