Files
onboard/app/Exports/StatisticsExport.php
2026-03-25 17:37:32 +09:00

410 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Exports;
use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
class StatisticsExport implements FromCollection, WithHeadings, WithMapping, WithStyles
{
protected array $data;
protected array $dateRange;
protected string $reportName;
public function __construct(array $data, array $dateRange, string $reportName = 'Статистика по отделениям')
{
$this->data = $data;
$this->dateRange = $dateRange;
$this->reportName = $reportName;
}
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return collect($this->data);
}
/**
* Заголовки (с вложенной структурой)
*/
public function headings(): array
{
return [
// Шапка отчета (первые 3 строки)
[$this->reportName],
['Дата создания: ' . now()->format('d.m.Y H:i:s')],
[$this->formatDateRange()],
[], // Пустая строка для отступа
// Первый уровень заголовков (с объединением)
[
'Отделение',
'Кол-во коек',
'Поступило', // Будет объединено с 4 колонками
'', '', '', // Пустые для заполнения
'Выбыло',
'Состоит',
'Ср. койко-день',
'Пред. опер. койко-день',
'% загруженности',
'% смертности',
'Операции', // Будет объединено с 2 колонками
'', // Пустые для заполнения
'Умерло',
'Мед. персонал'
],
// Второй уровень заголовков (детализация)
[
'',
'',
'Всего',
'План',
'Экстр',
'Перевод',
'',
'',
'',
'',
'',
'',
'Э',
'П',
'',
''
]
];
}
/**
* Форматирование дат для шапки
*/
protected function formatDateRange(): string
{
if (isset($this->dateRange[0]) && isset($this->dateRange[1])) {
$startAt = Carbon::create($this->dateRange[0])->format('d.m.Y H:i');
$endAt = Carbon::create($this->dateRange[1])->format('d.m.Y H:i');
return 'Период: ' . $startAt . ' - ' . $endAt;
}
return 'Период: За весь период';
}
/**
* Маппинг данных для каждой строки
*/
public function map($row): array
{
// Заголовок группы (Хирургические отделения, Терапевтические отделения)
if (isset($row['isGroupHeader']) && $row['isGroupHeader']) {
return [
$row['groupName'], // Название группы
'', '', '', '', '', '', '', '', '', '', '', '', ''
];
}
// Итоговая строка
if (isset($row['isTotalRow']) && $row['isTotalRow']) {
return [
$row['department'],
$row['beds'] ?? '',
$this->formatZero($row['recipients']['all'] ?? 0),
$this->formatZero($row['recipients']['plan'] ?? 0),
$this->formatZero($row['recipients']['emergency'] ?? 0),
$this->formatZero($row['recipients']['transferred'] ?? 0),
$this->formatZero($row['outcome'] ?? 0),
$this->formatZero($row['consist'] ?? 0),
$this->formatZero($row['averageBedDays'] ?? 0),
$this->formatZero($row['preoperativeDays'] ?? 0),
$this->formatZero($row['percentLoadedBeds'] ?? 0),
$this->formatZero($row['overallLethality'] ?? 0),
$this->formatZero($row['surgical']['emergency'] ?? 0),
$this->formatZero($row['surgical']['plan'] ?? 0),
$this->formatZero($row['deceased'] ?? 0),
$this->formatZero($row['countStaff'] ?? 0)
];
}
// Обычное отделение
return [
$row['department'] ?? '',
$this->formatZero($row['beds'] ?? 0),
$this->formatZero($row['recipients']['all'] ?? 0),
$this->formatZero($row['recipients']['plan'] ?? 0),
$this->formatZero($row['recipients']['emergency'] ?? 0),
$this->formatZero($row['recipients']['transferred'] ?? 0),
$this->formatZero($row['outcome'] ?? 0),
$this->formatZero($row['consist'] ?? 0),
$this->formatZero($row['averageBedDays'] ?? 0),
$this->formatZero($row['preoperativeDays'] ?? 0),
$this->formatZero($row['percentLoadedBeds'] ?? 0),
$this->formatZero($row['lethality'] ?? 0),
$this->formatZero($row['surgical']['emergency'] ?? 0),
$this->formatZero($row['surgical']['plan'] ?? 0),
$this->formatZero($row['deceased'] ?? 0),
$this->formatZero($row['countStaff'] ?? 0)
];
}
/**
* Стилизация Excel файла
*/
public function styles(Worksheet $sheet)
{
// Поля (уменьшаем для экономии места)
$sheet->getPageMargins()->setLeft(0.2);
$sheet->getPageMargins()->setRight(0.2);
$sheet->getPageMargins()->setTop(0.2);
$sheet->getPageMargins()->setBottom(0.2);
$sheet->getPageSetup()->setPaperSize(PageSetup::PAPERSIZE_A4);
$sheet->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
$sheet->getPageSetup()->setFitToPage(true);
$sheet->getPageSetup()->setFitToWidth(1); // 1 страница в ширину
$sheet->getPageSetup()->setFitToHeight(1); // 1 страница в высоту (опционально)
// $sheet->getPageSetup()->setScale(90);
$highestRow = $sheet->getHighestRow();
$highestColumn = $sheet->getHighestColumn();
// ОБЪЕДИНЕНИЕ ЯЧЕЕК ДЛЯ ШАПКИ (строки 1-3)
$sheet->mergeCells('A1:' . $highestColumn . '1'); // Наименование
$sheet->mergeCells('A2:' . $highestColumn . '2'); // Дата создания
$sheet->mergeCells('A3:' . $highestColumn . '3'); // Временной интервал
// ОБЪЕДИНЕНИЕ ДЛЯ ОБЫЧНЫХ ЗАГОЛОВКОВ
$sheet->mergeCells('A5:A6');
$sheet->mergeCells('B5:B6');
$sheet->mergeCells('G5:G6');
$sheet->mergeCells('H5:H6');
$sheet->mergeCells('I5:I6');
$sheet->mergeCells('J5:J6');
$sheet->mergeCells('K5:K6');
$sheet->mergeCells('L5:L6');
$sheet->mergeCells('O5:O6');
$sheet->mergeCells('P5:P6');
// ОБЪЕДИНЕНИЕ ДЛЯ ВЛОЖЕННЫХ ЗАГОЛОВКОВ
// Строка 5 (первый уровень заголовков)
$sheet->mergeCells('C5:F5'); // Объединяем "Поступило" (колонки C, D, E, F)
$sheet->mergeCells('M5:N5'); // Объединяем "Операции" (колонки M, N)
// Устанавливаем значения для объединенных ячеек
$sheet->setCellValue('C5', 'Поступило');
$sheet->setCellValue('M5', 'Операции');
// СТИЛИ ДЛЯ ШАПКИ ОТЧЕТА (строки 1-3)
$sheet->getStyle('A1:A3')->applyFromArray([
'font' => [
'bold' => true,
'size' => 14,
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_LEFT,
'vertical' => Alignment::VERTICAL_CENTER,
],
]);
// СТИЛИ ДЛЯ ЗАГОЛОВКОВ (строки 5 и 6)
$sheet->getStyle('A5:' . $highestColumn . '6')->applyFromArray([
'font' => [
'bold' => true,
'color' => ['argb' => '000000'],
'size' => 11,
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER,
'wrapText' => true,
],
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['argb' => '2d2d30'],
],
],
]);
// ДОПОЛНИТЕЛЬНЫЕ СТИЛИ ДЛЯ ОБЪЕДИНЕННЫХ ЗАГОЛОВКОВ
$sheet->getStyle('C5')->applyFromArray([
'font' => ['bold' => true, 'color' => ['argb' => '000000']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
$sheet->getStyle('M5')->applyFromArray([
'font' => ['bold' => true, 'color' => ['argb' => '000000']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
// СТИЛИ ДЛЯ ВСЕХ СТРОК С ДАННЫМИ
$indexData = 0;
for ($row = 7; $row <= $highestRow; $row++) {
$cellValue = $sheet->getCell('A' . $row)->getValue();
$currentRowInData = $this->data[$indexData];
// Заголовки групп (Хирургические отделения, Терапевтические отделения)
if (array_key_exists('isGroupHeader', $currentRowInData))
{
$sheet->getStyle('A' . $row . ':' . $highestColumn . $row)->applyFromArray([
'font' => [
'bold' => true,
'size' => 11,
],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'D9E1F2'], // Светло-синий
],
]);
// Объединяем ячейки для группы
$sheet->mergeCells('A' . $row . ':' . $highestColumn . $row);
}
// Итоговые строки (ИТОГО:)
if (array_key_exists('isTotalRow', $currentRowInData)) {
$sheet->getStyle('A' . $row . ':' . $highestColumn . $row)->applyFromArray([
'font' => [
'bold' => true,
],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'F2F2F2'], // Серый
],
'borders' => [
'top' => [
'borderStyle' => Border::BORDER_MEDIUM,
'color' => ['argb' => '000000'],
],
],
]);
}
if ($indexData < count($this->data))
$indexData++;
}
// ГРАНИЦЫ ДЛЯ ВСЕЙ ТАБЛИЦЫ
$sheet->getStyle('A5:' . $highestColumn . $highestRow)->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['argb' => '000000'],
],
],
]);
// ВЫРАВНИВАНИЕ ДЛЯ ЧИСЛОВЫХ КОЛОНОК
$sheet->getStyle('B7:' . $highestColumn . $highestRow)->applyFromArray([
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
],
]);
// ВЫРАВНИВАНИЕ ДЛЯ НАЗВАНИЙ ОТДЕЛЕНИЙ
$sheet->getStyle('A7:A' . $highestRow)->applyFromArray([
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_LEFT,
'vertical' => Alignment::VERTICAL_CENTER,
],
]);
// Устанавливаем ширину колонок
$sheet->getColumnDimension('A')->setWidth(25); // Отделение
$sheet->getColumnDimension('B')->setWidth(8); // Кол-во коек
$sheet->getColumnDimension('C')->setWidth(8); // Всего
$sheet->getColumnDimension('D')->setWidth(8); // План
$sheet->getColumnDimension('E')->setWidth(8); // Экстр
$sheet->getColumnDimension('F')->setWidth(10); // Перевод
$sheet->getColumnDimension('G')->setWidth(10); // Выбыло
$sheet->getColumnDimension('H')->setWidth(10); // Состоит
$sheet->getColumnDimension('I')->setWidth(10); // Ср. койко-день
$sheet->getColumnDimension('J')->setWidth(10); // Пред. опер. койко-день
$sheet->getColumnDimension('K')->setWidth(10); // % загруженности
$sheet->getColumnDimension('L')->setWidth(8); // % летальности
$sheet->getColumnDimension('M')->setWidth(8); // Операции Э
$sheet->getColumnDimension('N')->setWidth(8); // Операции П
$sheet->getColumnDimension('O')->setWidth(10); // Умерло
$sheet->getColumnDimension('P')->setWidth(10); // Мед. персонал
// Увеличиваем высоту строк с заголовками
$sheet->getRowDimension(5)->setRowHeight(30);
$sheet->getRowDimension(6)->setRowHeight(25);
// Добавляем примечание внизу
// $this->addFootnote($sheet, $highestRow, $highestColumn);
return [];
}
/**
* Форматирование нулевых значений
*/
protected function formatZero($value)
{
// Если значение null или пустая строка - возвращаем "0"
if (is_null($value) || $value === '' || $value === []) {
return '—';
}
// Если это массив (для recipients или surgical) - такого не должно быть,
// но на всякий случай обработаем
if (is_array($value)) {
return '0';
}
// Если это число с плавающей точкой и оно равно 0
if (is_numeric($value) && $value == 0) {
return '0';
}
return $value;
}
/**
* Добавить примечание в конец файла
*/
public function addFootnote(Worksheet $sheet, int $startRow, string $highestColumn): void
{
$footnoteRow = $startRow + 2; // Отступаем 2 строки от таблицы
$sheet->setCellValue('A' . $footnoteRow, 'ПРИМЕЧАНИЕ:');
$sheet->setCellValue('B' . $footnoteRow, '• % загруженности актуален на дату формирования отчёта (' . now()->format('d.m.Y') . ')');
$sheet->mergeCells('B' . $footnoteRow . ':' . $highestColumn . $footnoteRow);
$sheet->setCellValue('B' . ($footnoteRow + 1), '• Поступление, выбытие, операции — за указанный в шапке период');
$sheet->mergeCells('B' . ($footnoteRow + 1) . ':' . $highestColumn . ($footnoteRow + 1));
// Стили для примечания
$sheet->getStyle('A' . $footnoteRow . ':' . $highestColumn . ($footnoteRow + 1))->applyFromArray([
'font' => [
'italic' => true,
'size' => 9,
'color' => ['argb' => '666666'],
],
'alignment' => [
'vertical' => Alignment::VERTICAL_CENTER,
],
]);
// Жирный шрифт для слова "ПРИМЕЧАНИЕ"
$sheet->getStyle('A' . $footnoteRow)->applyFromArray([
'font' => [
'bold' => true,
'color' => ['argb' => '000000'],
],
]);
}
}