Files
onboard/app/Services/StatisticsService.php

691 lines
29 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\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(),
];
}
}