Добавил строку с итогами

Рефакторинг контроллера статистики
This commit is contained in:
brusnitsyn
2026-02-10 08:56:09 +09:00
parent 0b8a5ca216
commit 7329893775
3 changed files with 793 additions and 264 deletions

View File

@@ -0,0 +1,683 @@
<?php
namespace App\Services;
use App\Models\Department;
use App\Models\Report;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class StatisticsService
{
protected array $metricMapping = [
4 => 'plan', // Плановые поступления
12 => 'emergency', // Экстренные поступления
11 => 'plan_surgical', // Плановые операции
10 => 'emergency_surgical', // Экстренные операции
13 => 'transferred', // Переведенные
7 => 'outcome', // Выбыло
9 => 'deceased', // Умерло
8 => 'current', // Состоит
];
/**
* Получить статистические данные с оптимизацией
*/
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
// Определяем порог для использования оптимизированного метода
$daysDiff = Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate));
// Для диапазонов больше 30 дней используем агрегированные данные
if ($daysDiff > 30) {
return $this->getAggregatedStatistics($user, $startDate, $endDate, $isRangeOneDay);
}
// Для диапазонов 7-30 дней используем оптимизированный метод
if ($daysDiff > 7) {
return $this->getOptimizedStatistics($user, $startDate, $endDate, $isRangeOneDay);
}
// Для малых диапазонов используем детальный метод
return $this->getDetailedStatistics($user, $startDate, $endDate, $isRangeOneDay);
}
/**
* Агрегированный метод для очень больших диапазонов (больше 30 дней)
*/
private function getAggregatedStatistics(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
// Устанавливаем дату отчета
if ($isRangeOneDay) {
$dateReport = $endDate;
} else {
$dateReport = [$startDate, $endDate];
}
// Загружаем все отделения
$departments = Department::select('department_id', 'rf_department_type', 'name_short')
->with(['departmentType'])
->orderBy('name_short')
->get()
->keyBy('department_id');
// Загружаем метрики по умолчанию
$defaultMetrics = $this->getDefaultMetricsBatch($departments->pluck('department_id')->toArray());
// Получаем агрегированные данные по отчетам
$aggregatedData = $this->getAggregatedReportData(
$departments->pluck('department_id')->toArray(),
$dateReport,
$isRangeOneDay
);
// Получаем последние отчеты для текущих пациентов
$lastReportsData = $this->getLastReportsData(
$departments->pluck('department_id')->toArray(),
$isRangeOneDay ? $dateReport : $dateReport[1]
);
return $this->processAggregatedData(
$departments,
$defaultMetrics,
$aggregatedData,
$lastReportsData,
$isRangeOneDay
);
}
/**
* Получить агрегированные данные по отчетам
*/
private function getAggregatedReportData(array $departmentIds, $dateReport, bool $isRangeOneDay): Collection
{
$query = DB::table('reports as r')
->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id')
->select(
'r.rf_department_id',
'mr.rf_metrika_item_id',
DB::raw('SUM(CAST(mr.value AS INTEGER)) as total')
)
->whereIn('r.rf_department_id', $departmentIds);
if ($isRangeOneDay) {
$query->whereDate('r.created_at', $dateReport);
} else {
$query->whereBetween('r.created_at', $dateReport);
}
return $query->whereIn('mr.rf_metrika_item_id', array_keys($this->metricMapping))
->groupBy('r.rf_department_id', 'mr.rf_metrika_item_id')
->get()
->groupBy('rf_department_id');
}
/**
* Получить данные последних отчетов
*/
private function getLastReportsData(array $departmentIds, string $date): Collection
{
// Находим ID последних отчетов для каждого отделения
$subQuery = DB::table('reports')
->select('rf_department_id', DB::raw('MAX(report_id) as last_report_id'))
->whereIn('rf_department_id', $departmentIds)
->whereDate('created_at', '<=', $date)
->groupBy('rf_department_id');
return DB::table('metrika_results as mr')
->joinSub($subQuery, 'last_reports', function ($join) {
$join->on('mr.rf_report_id', '=', 'last_reports.last_report_id');
})
->where('mr.rf_metrika_item_id', 8) // Только текущие пациенты
->select('last_reports.rf_department_id', 'mr.value')
->get()
->keyBy('rf_department_id');
}
/**
* Обработать агрегированные данные
*/
private function processAggregatedData(
Collection $departments,
Collection $defaultMetrics,
Collection $aggregatedData,
Collection $lastReportsData,
bool $isRangeOneDay
): array {
$groupedData = [];
$totalsByType = [];
foreach ($departments as $department) {
$departmentId = $department->department_id;
$departmentType = $department->departmentType->name_full;
if (!isset($groupedData[$departmentType])) {
$groupedData[$departmentType] = [];
$totalsByType[$departmentType] = $this->initTypeTotals();
}
// Получаем агрегированные метрики
$metrics = $aggregatedData->get($departmentId, collect());
$counters = array_fill_keys(array_values($this->metricMapping), 0);
foreach ($metrics as $metric) {
$key = $this->metricMapping[$metric->rf_metrika_item_id] ?? null;
if ($key) {
// Для агрегированных данных всегда берем сумму
$counters[$key] = (int)$metric->total;
}
}
// Текущие пациенты из последнего отчета
$currentCount = (int)($lastReportsData->get($departmentId)?->value ?? 0);
$counters['current'] = $currentCount;
// Количество коек
$bedsCount = (int)($defaultMetrics->get($departmentId)?->value ?? 0);
// Рассчитываем значения
$allCount = $counters['plan'] + $counters['emergency'];
$percentLoadedBeds = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0;
// Формируем данные
$departmentData = $this->createDepartmentData(
$department->name_short,
$bedsCount,
$allCount,
$counters,
$percentLoadedBeds,
$departmentType
);
$groupedData[$departmentType][] = $departmentData;
$this->updateTypeTotals($totalsByType[$departmentType], $departmentData);
}
return $this->buildFinalData($groupedData, $totalsByType);
}
/**
* Оптимизированный метод для средних диапазонов (7-30 дней)
*/
private function getOptimizedStatistics(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
// Устанавливаем дату отчета
if ($isRangeOneDay) {
$dateReport = $endDate;
} else {
$dateReport = [$startDate, $endDate];
}
// Загружаем все отделения
$departments = Department::select('department_id', 'rf_department_type', 'name_short')
->with(['departmentType'])
->orderBy('name_short')
->get()
->keyBy('department_id');
// Загружаем метрики по умолчанию
$defaultMetrics = $this->getDefaultMetricsBatch($departments->pluck('department_id')->toArray());
// Загружаем отчеты
$reports = $this->getReportsBatch($departments->pluck('department_id')->toArray(), $dateReport, $isRangeOneDay);
// Загружаем метрики отчетов
$reportIds = $reports->flatMap(fn($items) => $items->pluck('report_id'))->toArray();
$reportMetrics = $this->getReportMetricsBatch($reportIds);
return $this->processOptimizedData(
$departments,
$defaultMetrics,
$reports,
$reportMetrics,
$dateReport,
$isRangeOneDay
);
}
/**
* Получить метрики по умолчанию для всех отделений пачкой
*/
private function getDefaultMetricsBatch(array $departmentIds): Collection
{
return DB::table('department_metrika_defaults')
->whereIn('rf_department_id', $departmentIds)
->where('rf_metrika_item_id', 1) // только койки
->select('rf_department_id', 'value')
->get()
->keyBy('rf_department_id');
}
/**
* Получить отчеты для всех отделений пачкой
*/
private function getReportsBatch(array $departmentIds, $dateReport, bool $isRangeOneDay): Collection
{
$query = Report::whereIn('rf_department_id', $departmentIds);
if ($isRangeOneDay) {
$query->whereDate('created_at', $dateReport);
} else {
$query->whereBetween('created_at', $dateReport);
}
return $query->select('report_id', 'rf_department_id', 'created_at')
->orderBy('created_at')
->get()
->groupBy('rf_department_id');
}
/**
* Получить метрики отчетов пачкой
*/
private function getReportMetricsBatch(array $reportIds): Collection
{
if (empty($reportIds)) {
return collect();
}
return DB::table('metrika_results')
->whereIn('rf_report_id', $reportIds)
->whereIn('rf_metrika_item_id', array_keys($this->metricMapping))
->select('rf_report_id', 'rf_metrika_item_id', 'value')
->get()
->groupBy('rf_report_id');
}
/**
* Обработать оптимизированные данные
*/
private function processOptimizedData(
Collection $departments,
Collection $defaultMetrics,
Collection $reports,
Collection $reportMetrics,
$dateReport,
bool $isRangeOneDay
): array {
$groupedData = [];
$totalsByType = [];
foreach ($departments as $department) {
$departmentId = $department->department_id;
$departmentType = $department->departmentType->name_full;
if (!isset($groupedData[$departmentType])) {
$groupedData[$departmentType] = [];
$totalsByType[$departmentType] = $this->initTypeTotals();
}
// Получаем отчеты отделения
$departmentReports = $reports->get($departmentId, collect());
$lastReport = $departmentReports->last();
// Инициализируем счетчики
$counters = array_fill_keys(array_values($this->metricMapping), 0);
// Обрабатываем каждый отчет
foreach ($departmentReports as $report) {
$metrics = $reportMetrics->get($report->report_id, collect())
->keyBy('rf_metrika_item_id');
foreach ($this->metricMapping as $metricId => $key) {
if ($metrics->has($metricId)) {
$value = (int)$metrics[$metricId]->value;
// Разная логика для одного дня и диапазона
if ($isRangeOneDay) {
// Для одного дня: суммируем
$counters[$key] += $value;
} else {
// Для диапазона:
if ($metricId === 8) {
// Для текущих пациентов берем ПОСЛЕДНЕЕ значение
if ($report === $lastReport) {
$counters[$key] = $value;
}
} else {
// Для остальных суммируем
$counters[$key] += $value;
}
}
}
}
}
// Если нет отчетов за день, но есть последний отчет ранее
if ($counters['current'] === 0 && $lastReport) {
$metrics = $reportMetrics->get($lastReport->report_id, collect());
$currentMetric = $metrics->firstWhere('rf_metrika_item_id', 8);
if ($currentMetric) {
$counters['current'] = (int)$currentMetric->value;
}
}
// Получаем количество коек
$bedsCount = (int)($defaultMetrics->get($departmentId)?->value ?? 0);
// Рассчитываем значения
$allCount = $counters['plan'] + $counters['emergency'];
$percentLoadedBeds = $bedsCount > 0 ? round($counters['current'] * 100 / $bedsCount) : 0;
// Формируем данные отделения
$departmentData = $this->createDepartmentData(
$department->name_short,
$bedsCount,
$allCount,
$counters,
$percentLoadedBeds,
$departmentType
);
$groupedData[$departmentType][] = $departmentData;
$this->updateTypeTotals($totalsByType[$departmentType], $departmentData);
}
return $this->buildFinalData($groupedData, $totalsByType);
}
/**
* Детальный метод для небольших диапазонов (до 7 дней)
*/
private function getDetailedStatistics(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
// Устанавливаем дату отчета
if ($isRangeOneDay) {
$dateReport = $endDate;
} else {
$dateReport = [$startDate, $endDate];
}
$groupedData = [];
$totalsByType = [];
$departments = Department::select('department_id', 'rf_department_type', 'name_short')
->with(['departmentType', 'reports' => function ($query) use ($dateReport, $isRangeOneDay) {
if ($isRangeOneDay) {
$query->whereDate('created_at', $dateReport);
} else {
$query->whereBetween('created_at', $dateReport);
}
$query->with('metrikaResults');
}])
->orderBy('name_short')
->get();
foreach ($departments as $department) {
$departmentType = $department->departmentType->name_full;
if (!isset($groupedData[$departmentType])) {
$groupedData[$departmentType] = [];
$totalsByType[$departmentType] = $this->initTypeTotals();
}
// Получаем отчеты
$reports = $department->reports;
$lastReport = $reports->last();
// Инициализируем счетчики
$counters = array_fill_keys(array_values($this->metricMapping), 0);
// Суммируем метрики
foreach ($reports as $report) {
foreach ($report->metrikaResults as $metric) {
$key = $this->metricMapping[$metric->rf_metrika_item_id] ?? null;
if ($key) {
$value = (int)$metric->value;
// ВАЖНО: разная логика для одного дня и диапазона
if ($isRangeOneDay) {
// Для одного дня: суммируем все значения
$counters[$key] += $value;
} else {
// Для диапазона:
if ($metric->rf_metrika_item_id === 8) {
// Для текущих пациентов берем ПОСЛЕДНЕЕ значение
// из последнего отчета за день
if ($report === $lastReport) {
$counters[$key] = $value;
}
} else {
// Для остальных метрик СУММИРУЕМ за весь период
$counters[$key] += $value;
}
}
}
}
}
// Получаем количество коек
$bedsCount = (int)$department->metrikaDefault()
->where('rf_metrika_item_id', 1)
->value('value') ?? 0;
// Рассчитываем итоговые значения
$allCount = $counters['plan'] + $counters['emergency'];
$percentLoadedBeds = $bedsCount > 0 ? round($counters['current'] * 100 / $bedsCount) : 0;
// Формируем данные отделения
$departmentData = $this->createDepartmentData(
$department->name_short,
$bedsCount,
$allCount,
$counters,
$percentLoadedBeds,
$departmentType
);
$groupedData[$departmentType][] = $departmentData;
$this->updateTypeTotals($totalsByType[$departmentType], $departmentData);
}
return $this->buildFinalData($groupedData, $totalsByType);
}
/**
* Создать данные отделения
*/
private function createDepartmentData(
string $name,
int $beds,
int $allCount,
array $counters,
int $percentLoadedBeds,
string $type
): array {
return [
'department' => $name,
'beds' => $beds,
'recipients' => [
'all' => $allCount,
'plan' => $counters['plan'],
'emergency' => $counters['emergency'],
'transferred' => $counters['transferred'],
],
'outcome' => $counters['outcome'],
'consist' => $counters['current'],
'percentLoadedBeds' => $percentLoadedBeds,
'surgical' => [
'plan' => $counters['plan_surgical'],
'emergency' => $counters['emergency_surgical']
],
'deceased' => $counters['deceased'],
'type' => $type,
'isDepartment' => true
];
}
/**
* Инициализировать итоги по типу
*/
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,
];
}
/**
* Обновить итоги по типу
*/
private function updateTypeTotals(array &$totals, array $departmentData): void
{
$totals['departments_count']++;
$totals['beds_sum'] += $departmentData['beds'];
$totals['recipients_all_sum'] += $departmentData['recipients']['all'];
$totals['recipients_plan_sum'] += $departmentData['recipients']['plan'];
$totals['recipients_emergency_sum'] += $departmentData['recipients']['emergency'];
$totals['recipients_transferred_sum'] += $departmentData['recipients']['transferred'];
$totals['outcome_sum'] += $departmentData['outcome'];
$totals['consist_sum'] += $departmentData['consist'];
$totals['plan_surgical_sum'] += $departmentData['surgical']['plan'];
$totals['emergency_surgical_sum'] += $departmentData['surgical']['emergency'];
$totals['deceased_sum'] += $departmentData['deceased'];
$totals['percentLoadedBeds_total'] += $departmentData['percentLoadedBeds'];
$totals['percentLoadedBeds_count']++;
}
/**
* Построить финальные данные с итогами
*/
private function buildFinalData(array $groupedData, array $totalsByType): array
{
$finalData = [];
$grandTotals = $this->initTypeTotals();
foreach ($groupedData as $type => $departmentsInType) {
// Добавляем заголовок группы
$finalData[] = [
'isGroupHeader' => true,
'groupName' => $type,
'colspan' => 12,
'type' => $type
];
// Добавляем отделения
foreach ($departmentsInType as $department) {
$finalData[] = $department;
}
// Добавляем итоги по группе
if (!empty($departmentsInType) && isset($totalsByType[$type])) {
$total = $totalsByType[$type];
$avgPercent = $total['percentLoadedBeds_count'] > 0
? round($total['percentLoadedBeds_total'] / $total['percentLoadedBeds_count'])
: 0;
$finalData[] = $this->createTotalRow($type, $total, $avgPercent, false);
// Обновляем общие итоги
$this->updateGrandTotals($grandTotals, $total);
}
}
// Добавляем общие итоги
// if ($grandTotals['departments_count'] > 0) {
// $avgPercent = $grandTotals['percentLoadedBeds_count'] > 0
// ? round($grandTotals['percentLoadedBeds_total'] / $grandTotals['percentLoadedBeds_count'])
// : 0;
//
// $finalData[] = $this->createTotalRow('all', $grandTotals, $avgPercent, true);
// }
return [
'data' => $finalData,
'totalsByType' => $totalsByType,
'grandTotals' => $grandTotals
];
}
/**
* Создать строку итогов
*/
private function createTotalRow(string $type, array $total, int $avgPercent, bool $isGrandTotal): array
{
$row = [
'isTotalRow' => !$isGrandTotal,
'isGrandTotal' => $isGrandTotal,
'department' => $isGrandTotal
? 'ОБЩИЕ ИТОГИ:'
: 'ИТОГО:',
'beds' => '—',//$total['beds_sum'],
'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' => '—',//$avgPercent,
'surgical' => [
'plan' => $total['plan_surgical_sum'],
'emergency' => $total['emergency_surgical_sum']
],
'deceased' => $total['deceased_sum'],
'type' => $type,
'departments_count' => $total['departments_count'],
'isBold' => true
];
if ($isGrandTotal) {
$row['backgroundColor'] = '#f0f8ff';
}
return $row;
}
/**
* Обновить общие итоги
*/
private function updateGrandTotals(array &$grandTotals, array $typeTotal): void
{
foreach ($grandTotals as $key => &$value) {
if (isset($typeTotal[$key])) {
$value += $typeTotal[$key];
}
}
}
/**
* Очистить кэш статистики при создании отчета
*/
public function clearStatisticsCache(User $user, ?string $date = null): void
{
// Очищаем кэш по тегам
Cache::tags([
'statistics',
'reports',
'department_' . $user->rf_department_id
])->flush();
\Log::info("Statistics cache cleared for department: " . $user->rf_department_id);
}
/**
* Очистить кэш статистики для всех пользователей отдела
*/
public function clearDepartmentStatisticsCache(int $departmentId): void
{
Cache::tags([
'statistics',
'reports',
'department_' . $departmentId
])->flush();
\Log::info("Statistics cache cleared for entire department: " . $departmentId);
}
}