691 lines
29 KiB
PHP
691 lines
29 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\ReportDuty;
|
||
use App\Models\User;
|
||
use App\Models\UserDepartment;
|
||
use App\Services\MetricCalculators\PlanCalculator;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class StatisticsService
|
||
{
|
||
/**
|
||
* Справочник типов оплаты (profit_type_id) — берётся из классификатора МИС (kl_ProfitType).
|
||
*/
|
||
public const PROFIT_TYPE_LABELS = [
|
||
0 => 'Не определено',
|
||
3 => 'ОМС',
|
||
4 => 'Бюджет',
|
||
5 => 'Платные услуги',
|
||
6 => 'ДМС',
|
||
7 => 'Другие',
|
||
];
|
||
|
||
public function __construct(
|
||
protected BedDayService $bedDayService,
|
||
protected PlanCalculator $planCalculator,
|
||
) {}
|
||
|
||
/**
|
||
* Отделения, доступные пользователю, сгруппированные по типу отделения.
|
||
*/
|
||
private function resolveUserDepartments(User $user)
|
||
{
|
||
return 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');
|
||
}
|
||
|
||
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
|
||
{
|
||
$this->bedDayService->clearMemoryCache();
|
||
|
||
// 1. Получаем отделения
|
||
$departments = $this->resolveUserDepartments($user);
|
||
|
||
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 = [];
|
||
|
||
$departmentsPlans = $this->planCalculator->calculate($allDeptIds, $startDate, $endDate);
|
||
|
||
$grandRecipientPlan = collect($departmentsPlans)->sum('cumulative_plan');
|
||
$grandProgressPlan = collect($departmentsPlans)->sum('actual_year_to_date');
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
$grandTotals = $this->calculateGrandTotals($totalsByType);
|
||
$bedsTotal = (int) collect($beds)->sum('value');
|
||
// Посуточные ряды для фон-спарклайнов KPI-карточек
|
||
$grandTotals['sparklines'] = $this->calculateDailySparklines(
|
||
$allDeptIds,
|
||
$startDate,
|
||
$endDate,
|
||
$bedsTotal
|
||
);
|
||
// KPI за предыдущий аналогичный период (для бейджей тренда)
|
||
$grandTotals['previous'] = $this->calculatePreviousKpis($allDeptIds, $startDate, $endDate, $bedsTotal);
|
||
|
||
return [
|
||
'data' => $this->buildFinalData($groupedData, $totalsByType),
|
||
'totalsByType' => $totalsByType,
|
||
'grandTotals' => $grandTotals,
|
||
'recipientPlanOfYear' => [
|
||
'plan' => $grandRecipientPlan, // Сумма планов по периоду
|
||
'progress' => $grandProgressPlan, // Сумма фактов по периоду
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Количество поступлений по типу оплаты (profit_type_id), с разбивкой по отделениям.
|
||
* "Поступление" определяется так же, как метрика "Поступило" на основном листе:
|
||
* дата поступления миграции пациента попадает в период конкретного дежурного отчёта.
|
||
*/
|
||
public function getProfitTypeBreakdown(User $user, string $startDate, string $endDate): array
|
||
{
|
||
$departments = $this->resolveUserDepartments($user);
|
||
|
||
if ($departments->isEmpty()) {
|
||
return $this->emptyProfitTypeResponse();
|
||
}
|
||
|
||
$allDeptIds = $departments->flatten()->pluck('department_id')->toArray();
|
||
|
||
$rows = DB::table('report_duties as rd')
|
||
->join('report_duty_patients as rdp', 'rdp.report_duty_id', '=', 'rd.id')
|
||
->join('report_duty_migration_patients as rdmp', 'rdmp.medical_history_id', '=', 'rdp.id')
|
||
->whereIn('rd.rf_department_id', $allDeptIds)
|
||
->where('rd.period_start', '>=', $startDate)
|
||
->where('rd.period_end', '<=', $endDate)
|
||
->whereColumn('rdmp.ingoing_date', '>', 'rd.period_start')
|
||
->whereColumn('rdmp.ingoing_date', '<=', 'rd.period_end')
|
||
->select('rd.rf_department_id', 'rdp.profit_type_id', DB::raw('COUNT(DISTINCT rdp.id) as count'))
|
||
->groupBy('rd.rf_department_id', 'rdp.profit_type_id')
|
||
->get();
|
||
|
||
// Поддерживаем коды, которых пока нет в справочнике (на случай новых типов из МИС)
|
||
$profitTypeLabels = self::PROFIT_TYPE_LABELS;
|
||
foreach ($rows->pluck('profit_type_id')->unique() as $profitTypeId) {
|
||
$profitTypeId = (int) $profitTypeId;
|
||
if (! array_key_exists($profitTypeId, $profitTypeLabels)) {
|
||
$profitTypeLabels[$profitTypeId] = "Тип оплаты #{$profitTypeId}";
|
||
}
|
||
}
|
||
|
||
$countsByDept = $rows->groupBy('rf_department_id');
|
||
$emptyCounts = array_fill_keys(array_keys($profitTypeLabels), 0);
|
||
|
||
$data = [];
|
||
$grandTotals = $emptyCounts;
|
||
|
||
foreach ($departments as $typeName => $deptList) {
|
||
$groupTotals = $emptyCounts;
|
||
$groupRows = [];
|
||
|
||
foreach ($deptList as $dept) {
|
||
$deptId = $dept->department_id;
|
||
$counts = $emptyCounts;
|
||
|
||
foreach ($countsByDept->get($deptId, collect()) as $row) {
|
||
$counts[(int) $row->profit_type_id] = (int) $row->count;
|
||
}
|
||
|
||
$groupRows[] = [
|
||
'department' => $dept->user_name ?? $dept->name_short,
|
||
'counts' => $counts,
|
||
'total' => array_sum($counts),
|
||
];
|
||
|
||
foreach ($counts as $profitTypeId => $count) {
|
||
$groupTotals[$profitTypeId] += $count;
|
||
$grandTotals[$profitTypeId] += $count;
|
||
}
|
||
}
|
||
|
||
$data[] = ['isGroupHeader' => true, 'groupName' => $typeName];
|
||
array_push($data, ...$groupRows);
|
||
$data[] = [
|
||
'isTotalRow' => true,
|
||
'department' => 'ИТОГО:',
|
||
'counts' => $groupTotals,
|
||
'total' => array_sum($groupTotals),
|
||
];
|
||
}
|
||
|
||
return [
|
||
'data' => $data,
|
||
'grandTotals' => [
|
||
'department' => 'ОБЩИЕ ИТОГИ:',
|
||
'counts' => $grandTotals,
|
||
'total' => array_sum($grandTotals),
|
||
],
|
||
'profitTypes' => $profitTypeLabels,
|
||
];
|
||
}
|
||
|
||
private function emptyProfitTypeResponse(): array
|
||
{
|
||
return [
|
||
'data' => [],
|
||
'grandTotals' => [
|
||
'department' => 'ОБЩИЕ ИТОГИ:',
|
||
'counts' => array_fill_keys(array_keys(self::PROFIT_TYPE_LABELS), 0),
|
||
'total' => 0,
|
||
],
|
||
'profitTypes' => self::PROFIT_TYPE_LABELS,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Посуточные ряды по каждому KPI за период — для фоновых спарклайнов.
|
||
* Источник — суточные дежурные отчёты (report_duties + duty_report_metric_results).
|
||
*/
|
||
private function calculateDailySparklines(array $deptIds, string $startDate, string $endDate, int $bedsTotal): array
|
||
{
|
||
if (empty($deptIds)) {
|
||
return ['days' => [], 'consist' => [], 'admissions' => [], 'outcome' => [], 'operations' => [], 'deceased' => [], 'load' => []];
|
||
}
|
||
|
||
$rows = DB::table('report_duties as r')
|
||
->join('duty_report_metric_results as mr', 'r.id', '=', 'mr.rf_report_id')
|
||
->whereIn('r.rf_department_id', $deptIds)
|
||
->where('r.period_end', '>=', $startDate)
|
||
->where('r.period_end', '<=', $endDate)
|
||
->whereIn('mr.rf_metrika_item_id', [4, 7, 8, 9, 10, 11, 12])
|
||
->select(
|
||
DB::raw('DATE(r.period_end) as day'),
|
||
'mr.rf_metrika_item_id as metric',
|
||
DB::raw('SUM(CAST(mr.value AS DECIMAL)) as total')
|
||
)
|
||
->groupBy(DB::raw('DATE(r.period_end)'), 'mr.rf_metrika_item_id')
|
||
->orderBy('day')
|
||
->get();
|
||
|
||
// day => [metric_id => total]
|
||
$byDay = [];
|
||
foreach ($rows as $row) {
|
||
$byDay[(string) $row->day][(int) $row->metric] = (float) $row->total;
|
||
}
|
||
ksort($byDay);
|
||
|
||
$days = array_keys($byDay);
|
||
$consist = $admissions = $outcome = $operations = $deceased = $load = [];
|
||
foreach ($byDay as $m) {
|
||
$c = (int) ($m[8] ?? 0);
|
||
$consist[] = $c;
|
||
$admissions[] = (int) (($m[4] ?? 0) + ($m[12] ?? 0));
|
||
$outcome[] = (int) ($m[7] ?? 0);
|
||
$operations[] = (int) (($m[11] ?? 0) + ($m[10] ?? 0));
|
||
$deceased[] = (int) ($m[9] ?? 0);
|
||
$load[] = $bedsTotal > 0 ? (int) round($c / $bedsTotal * 100) : 0;
|
||
}
|
||
|
||
return compact('days', 'consist', 'admissions', 'outcome', 'operations', 'deceased', 'load');
|
||
}
|
||
|
||
/**
|
||
* KPI за предыдущий аналогичный период: столько же, вплотную перед выбранным.
|
||
* Сдвигаем оба конца назад на (длина периода + 1 день) — так предыдущий идёт встык,
|
||
* без нахлёста (для одних суток сравнение с предыдущими сутками).
|
||
*/
|
||
private function calculatePreviousKpis(array $deptIds, string $startDate, string $endDate, int $bedsTotal): array
|
||
{
|
||
$start = Carbon::parse($startDate);
|
||
$end = Carbon::parse($endDate);
|
||
|
||
$shiftSeconds = $start->diffInSeconds($end) + 86400;
|
||
|
||
$prevStart = $start->copy()->subSeconds($shiftSeconds);
|
||
$prevEnd = $end->copy()->subSeconds($shiftSeconds);
|
||
|
||
return $this->kpiTotalsForRange(
|
||
$deptIds,
|
||
$prevStart->format('Y-m-d H:i:s'),
|
||
$prevEnd->format('Y-m-d H:i:s'),
|
||
$bedsTotal
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Сводные KPI за произвольный диапазон (суммы метрик + состав на конец периода).
|
||
*/
|
||
private function kpiTotalsForRange(array $deptIds, string $start, string $end, int $bedsTotal): array
|
||
{
|
||
$empty = ['consist' => 0, 'admissions' => 0, 'outcome' => 0, 'operations' => 0, 'deceased' => 0, 'load' => 0];
|
||
if (empty($deptIds)) {
|
||
return $empty;
|
||
}
|
||
|
||
// Суммы метрик за период
|
||
$sums = DB::table('report_duties as r')
|
||
->join('duty_report_metric_results as mr', 'r.id', '=', 'mr.rf_report_id')
|
||
->whereIn('r.rf_department_id', $deptIds)
|
||
->where('r.period_start', '>=', $start)
|
||
->where('r.period_end', '<=', $end)
|
||
->whereIn('mr.rf_metrika_item_id', [4, 7, 9, 10, 11, 12])
|
||
->select('mr.rf_metrika_item_id as metric', DB::raw('SUM(CAST(mr.value AS DECIMAL)) as total'))
|
||
->groupBy('mr.rf_metrika_item_id')
|
||
->get()
|
||
->keyBy(fn ($r) => (int) $r->metric);
|
||
|
||
$g = fn ($id) => (int) ($sums[$id]->total ?? 0);
|
||
|
||
// Состав на конец периода: последний отчёт по каждому отделению ≤ end, суммируем
|
||
$consistRows = DB::table('report_duties as r')
|
||
->join('duty_report_metric_results as mr', 'r.id', '=', 'mr.rf_report_id')
|
||
->whereIn('r.rf_department_id', $deptIds)
|
||
->where('mr.rf_metrika_item_id', 8)
|
||
->where('r.period_end', '<=', $end)
|
||
->select('r.rf_department_id', 'mr.value')
|
||
->orderBy('r.rf_department_id')
|
||
->orderBy('r.period_end', 'desc')
|
||
->distinct('r.rf_department_id')
|
||
->get();
|
||
$consist = (int) $consistRows->sum(fn ($x) => (float) $x->value);
|
||
|
||
return [
|
||
'consist' => $consist,
|
||
'admissions' => $g(4) + $g(12),
|
||
'outcome' => $g(7),
|
||
'operations' => $g(11) + $g(10),
|
||
'deceased' => $g(9),
|
||
'load' => $bedsTotal > 0 ? (int) round($consist / $bedsTotal * 100) : 0,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Инициализация итогов по типу
|
||
*/
|
||
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(),
|
||
];
|
||
}
|
||
}
|