894 lines
35 KiB
PHP
894 lines
35 KiB
PHP
<?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 $summableMetrics = [4, 12, 11, 10, 13, 7, 9, 8, 17, 14, 16];
|
||
|
||
/**
|
||
* Метрики, которые нужно усреднять (числа с плавающей точкой)
|
||
*/
|
||
protected array $averageMetrics = [18];
|
||
|
||
protected array $metricMapping = [
|
||
4 => 'plan', // Плановые поступления
|
||
12 => 'emergency', // Экстренные поступления
|
||
11 => 'plan_surgical', // Плановые операции
|
||
10 => 'emergency_surgical', // Экстренные операции
|
||
13 => 'transferred', // Переведенные
|
||
7 => 'outcome', // Выбыло
|
||
9 => 'deceased', // Умерло
|
||
8 => 'current', // Состоит
|
||
17 => 'count_staff', // Кол-во мед. персонала
|
||
14 => 'count_observable', // Кол-во пациентов на контроле
|
||
16 => 'count_unwanted', // Кол-во нежелательных событий
|
||
18 => 'average_bed_days' // Средний койко-день
|
||
];
|
||
|
||
public function __construct(
|
||
protected BedDayService $bedDayService
|
||
) { }
|
||
|
||
/**
|
||
* Получить статистические данные с оптимизацией
|
||
*/
|
||
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
|
||
{
|
||
// Очищаем кэш памяти перед началом
|
||
$this->bedDayService->clearMemoryCache();
|
||
|
||
// Определяем порог для использования оптимизированного метода
|
||
$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);
|
||
}
|
||
|
||
/**
|
||
* Получить средние койко-дни за период из сохраненных метрик отчетов
|
||
*/
|
||
protected function getAverageBedDaysFromReports(array $departmentIds, string $startDate, string $endDate): array
|
||
{
|
||
if (empty($departmentIds)) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
$results = DB::table('reports as r')
|
||
->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id')
|
||
->whereIn('r.rf_department_id', $departmentIds)
|
||
->where('mr.rf_metrika_item_id', 18)
|
||
->whereBetween('r.created_at', [$startDate, $endDate])
|
||
->select(
|
||
'r.rf_department_id',
|
||
DB::raw('AVG(CAST(mr.value AS DECIMAL)) as avg_value')
|
||
)
|
||
->groupBy('r.rf_department_id')
|
||
->get()
|
||
->keyBy('rf_department_id');
|
||
|
||
$averages = [];
|
||
foreach ($departmentIds as $departmentId) {
|
||
if (isset($results[$departmentId]) && $results[$departmentId]->avg_value !== null) {
|
||
$averages[$departmentId] = round((float)$results[$departmentId]->avg_value, 1);
|
||
} else {
|
||
$averages[$departmentId] = 0;
|
||
}
|
||
}
|
||
|
||
return $averages;
|
||
} catch (\Exception $e) {
|
||
\Log::error("Error in getAverageBedDaysFromReports: " . $e->getMessage());
|
||
return array_fill_keys($departmentIds, 0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получить общий средний койко-день
|
||
*/
|
||
protected function getOverallAverageBedDays(array $averages): float
|
||
{
|
||
$total = 0;
|
||
$count = 0;
|
||
|
||
foreach ($averages as $avg) {
|
||
if ($avg > 0) {
|
||
$total += $avg;
|
||
$count++;
|
||
}
|
||
}
|
||
|
||
return $count > 0 ? round($total / $count, 1) : 0;
|
||
}
|
||
|
||
/**
|
||
* Агрегированный метод для очень больших диапазонов (больше 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('rf_department_type')
|
||
->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]
|
||
);
|
||
|
||
// Получаем средние койко-дни из метрик отчетов
|
||
$averageBedDays = $this->getAverageBedDaysFromReports(
|
||
$departments->pluck('department_id')->toArray(),
|
||
$startDate,
|
||
$endDate
|
||
);
|
||
|
||
// Общий средний койко-день
|
||
$overallAverageBedDays = $this->getOverallAverageBedDays($averageBedDays);
|
||
|
||
return $this->processAggregatedData(
|
||
$departments,
|
||
$defaultMetrics,
|
||
$aggregatedData,
|
||
$lastReportsData,
|
||
$isRangeOneDay,
|
||
$averageBedDays,
|
||
$overallAverageBedDays
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Получить агрегированные данные по отчетам
|
||
*/
|
||
private function getAggregatedReportData(array $departmentIds, $dateReport, bool $isRangeOneDay): Collection
|
||
{
|
||
// Для суммируемых метрик - SUM
|
||
$summableQuery = 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 DECIMAL)) as total')
|
||
)
|
||
->whereIn('r.rf_department_id', $departmentIds)
|
||
->whereIn('mr.rf_metrika_item_id', $this->summableMetrics);
|
||
|
||
if ($isRangeOneDay) {
|
||
$summableQuery->whereDate('r.created_at', $dateReport);
|
||
} else {
|
||
$summableQuery->whereBetween('r.created_at', $dateReport);
|
||
}
|
||
|
||
$summableResults = $summableQuery
|
||
->groupBy('r.rf_department_id', 'mr.rf_metrika_item_id')
|
||
->get();
|
||
|
||
// Для усредняемых метрик - AVG (например, средний койко-день)
|
||
$averageQuery = 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('AVG(CAST(mr.value AS DECIMAL)) as total')
|
||
)
|
||
->whereIn('r.rf_department_id', $departmentIds)
|
||
->whereIn('mr.rf_metrika_item_id', $this->averageMetrics);
|
||
|
||
if ($isRangeOneDay) {
|
||
$averageQuery->whereDate('r.created_at', $dateReport);
|
||
} else {
|
||
$averageQuery->whereBetween('r.created_at', $dateReport);
|
||
}
|
||
|
||
$averageResults = $averageQuery
|
||
->groupBy('r.rf_department_id', 'mr.rf_metrika_item_id')
|
||
->get();
|
||
|
||
// Объединяем результаты
|
||
$allResults = $summableResults->concat($averageResults);
|
||
|
||
return $allResults->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 $averageBedDays,
|
||
float $overallAverageBedDays
|
||
): 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;
|
||
|
||
// Получаем средний койко-день для отделения
|
||
$departmentAverageBedDays = $averageBedDays[$departmentId] ?? 0;
|
||
|
||
// Формируем данные
|
||
$departmentData = $this->createDepartmentData(
|
||
$department->name_short,
|
||
$departmentId,
|
||
$bedsCount,
|
||
$allCount,
|
||
$counters,
|
||
$percentLoadedBeds,
|
||
$departmentType,
|
||
null,
|
||
$departmentAverageBedDays,
|
||
$overallAverageBedDays
|
||
);
|
||
|
||
$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('rf_department_type')
|
||
->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);
|
||
|
||
// Получаем средние койко-дни из метрик отчетов
|
||
$averageBedDays = $this->getAverageBedDaysFromReports(
|
||
$departments->pluck('department_id')->toArray(),
|
||
$startDate,
|
||
$endDate
|
||
);
|
||
|
||
// Общий средний койко-день
|
||
$overallAverageBedDays = $this->getOverallAverageBedDays($averageBedDays);
|
||
|
||
return $this->processOptimizedData(
|
||
$departments,
|
||
$defaultMetrics,
|
||
$reports,
|
||
$reportMetrics,
|
||
$dateReport,
|
||
$isRangeOneDay,
|
||
$averageBedDays,
|
||
$overallAverageBedDays
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Получить метрики по умолчанию для всех отделений пачкой
|
||
*/
|
||
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();
|
||
}
|
||
|
||
// Получаем все метрики, но для 18 не преобразуем в integer
|
||
$results = 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');
|
||
|
||
// Преобразуем значения в зависимости от типа метрики
|
||
foreach ($results as $reportId => $metrics) {
|
||
foreach ($metrics as $metric) {
|
||
if (in_array($metric->rf_metrika_item_id, $this->summableMetrics)) {
|
||
$metric->value = (int)$metric->value;
|
||
} else {
|
||
$metric->value = (float)$metric->value;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
/**
|
||
* Обработать оптимизированные данные
|
||
*/
|
||
private function processOptimizedData(
|
||
Collection $departments,
|
||
Collection $defaultMetrics,
|
||
Collection $reports,
|
||
Collection $reportMetrics,
|
||
$dateReport,
|
||
bool $isRangeOneDay,
|
||
array $averageBedDays,
|
||
float $overallAverageBedDays
|
||
): 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;
|
||
|
||
// Получаем средний койко-день для отделения
|
||
$departmentAverageBedDays = $averageBedDays[$departmentId] ?? 0;
|
||
|
||
// Формируем данные отделения
|
||
$departmentData = $this->createDepartmentData(
|
||
$department->name_short,
|
||
$departmentId,
|
||
$bedsCount,
|
||
$allCount,
|
||
$counters,
|
||
$percentLoadedBeds,
|
||
$departmentType,
|
||
null,
|
||
$departmentAverageBedDays,
|
||
$overallAverageBedDays
|
||
);
|
||
|
||
$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('rf_department_type')
|
||
->get();
|
||
|
||
// Получаем средние койко-дни из метрик отчетов
|
||
$averageBedDays = $this->getAverageBedDaysFromReports(
|
||
$departments->pluck('department_id')->toArray(),
|
||
$startDate,
|
||
$endDate
|
||
);
|
||
|
||
// Общий средний койко-день
|
||
$overallAverageBedDays = $this->getOverallAverageBedDays($averageBedDays);
|
||
|
||
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;
|
||
|
||
if ($isRangeOneDay)
|
||
$isReportToday = !empty($lastReport);
|
||
else $isReportToday = null;
|
||
|
||
$departmentAverageBedDays = $averageBedDays[$department->department_id] ?? 0;
|
||
|
||
// Формируем данные отделения
|
||
$departmentData = $this->createDepartmentData(
|
||
$department->name_short,
|
||
$department->department_id,
|
||
$bedsCount,
|
||
$allCount,
|
||
$counters,
|
||
$percentLoadedBeds,
|
||
$departmentType,
|
||
$isReportToday,
|
||
$departmentAverageBedDays,
|
||
$overallAverageBedDays
|
||
);
|
||
|
||
$groupedData[$departmentType][] = $departmentData;
|
||
$this->updateTypeTotals($totalsByType[$departmentType], $departmentData);
|
||
}
|
||
|
||
return $this->buildFinalData($groupedData, $totalsByType);
|
||
}
|
||
|
||
/**
|
||
* Создать данные отделения
|
||
*/
|
||
private function createDepartmentData(
|
||
string $name,
|
||
int $departmentId,
|
||
int $beds,
|
||
int $allCount,
|
||
array $counters,
|
||
int $percentLoadedBeds,
|
||
string $type,
|
||
?bool $isReportToday = null,
|
||
float $departmentAverageBedDays = 0,
|
||
float $overallAverageBedDays = 0
|
||
): array {
|
||
return [
|
||
'department' => $name,
|
||
'department_id' => $departmentId,
|
||
'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'],
|
||
'countStaff' => $counters['count_staff'],
|
||
'countObservable' => $counters['count_observable'],
|
||
'countUnwanted' => $counters['count_unwanted'],
|
||
'averageBedDays' => $departmentAverageBedDays,
|
||
'overallAverageBedDays' => $overallAverageBedDays,
|
||
'type' => $type,
|
||
'isDepartment' => true,
|
||
'isReportToday' => $isReportToday,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Инициализировать итоги по типу
|
||
*/
|
||
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,
|
||
'average_bed_days_total' => 0,
|
||
'average_bed_days_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']++;
|
||
$totals['staff_sum'] += $departmentData['countStaff'];
|
||
$totals['observable_sum'] += $departmentData['countObservable'];
|
||
$totals['unwanted_sum'] += $departmentData['countUnwanted'];
|
||
$totals['average_bed_days_total'] += $departmentData['averageBedDays'];
|
||
$totals['average_bed_days_count']++;
|
||
}
|
||
|
||
/**
|
||
* Построить финальные данные с итогами
|
||
*/
|
||
private function buildFinalData(array $groupedData, array $totalsByType): array
|
||
{
|
||
$finalData = [];
|
||
$grandTotals = $this->initTypeTotals();
|
||
|
||
foreach ($groupedData as $type => $departmentsInType) {
|
||
// Добавляем заголовок группы
|
||
$finalData[] = [
|
||
'isGroupHeader' => true,
|
||
'groupName' => $type,
|
||
'colspan' => 14,
|
||
'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;
|
||
//
|
||
// $grandAvgBedDays = $grandTotals['averageBedDays_count'] > 0
|
||
// ? round($grandTotals['averageBedDays_total'] / $grandTotals['averageBedDays_count'], 1)
|
||
// : $overallAverageBedDays;
|
||
//
|
||
// $finalData[] = $this->createTotalRow('all', $grandTotals, $avgPercent, true, $grandAvgBedDays);
|
||
// }
|
||
|
||
return [
|
||
'data' => $finalData,
|
||
'totalsByType' => $totalsByType,
|
||
'grandTotals' => $grandTotals
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Создать строку итогов
|
||
*/
|
||
private function createTotalRow(string $type, array $total, int $avgPercent, bool $isGrandTotal, float $avgBedDays = 0): 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'],
|
||
'averageBedDays' => '—',
|
||
'type' => $type,
|
||
'departments_count' => $total['departments_count'],
|
||
'countStaff' => $total['staff_sum'],
|
||
'countObservable' => $total['observable_sum'],
|
||
'countUnwanted' => $total['unwanted_sum'],
|
||
'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);
|
||
}
|
||
}
|