Профиль хирургии

This commit is contained in:
brusnitsyn
2026-03-25 17:37:32 +09:00
parent 52a80ccd3b
commit f566ab96df
75 changed files with 3841 additions and 1009 deletions

View File

@@ -0,0 +1,409 @@
<?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'],
],
]);
}
}