'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); } }