diff --git a/app/Services/StatisticsService.php b/app/Services/StatisticsService.php index ba0cfdf..e3e106b 100644 --- a/app/Services/StatisticsService.php +++ b/app/Services/StatisticsService.php @@ -266,10 +266,22 @@ class StatisticsService } } + $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' => $this->calculateGrandTotals($totalsByType), + 'grandTotals' => $grandTotals, 'recipientPlanOfYear' => [ 'plan' => $grandRecipientPlan, // Сумма планов по периоду 'progress' => $grandProgressPlan, // Сумма фактов по периоду @@ -277,6 +289,123 @@ class StatisticsService ]; } + /** + * Посуточные ряды по каждому 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, + ]; + } + /** * Инициализация итогов по типу */