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