Доработал отчет для экономистов

This commit is contained in:
brusnitsyn
2026-06-25 17:04:42 +09:00
parent 7495b0e3cb
commit d322317d06
6 changed files with 873 additions and 396 deletions

View File

@@ -0,0 +1,296 @@
<?php
namespace App\Exports\Sheets;
use App\Models\User;
use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class StatisticsEconomistData implements FromArray, WithHeadings, WithMapping, WithStyles, WithTitle
{
/**
* Сводная строка + строки по отделениям, в порядке отображения на листе.
*/
private array $allRows;
public function __construct(
private string $sheetTitle,
private string $reportName,
private array $rows,
private array $dateRange,
private User $user,
private array $grandTotals = [],
private array $profitTypes = []
) {
$summary = [
['isGroupHeader' => true, 'groupName' => 'СВОДНАЯ ИНФОРМАЦИЯ (ВСЕ ОТДЕЛЕНИЯ)'],
array_merge(['isTotalRow' => true], $this->grandTotals),
];
$this->allRows = array_merge($summary, $this->rows);
}
public function title(): string
{
return $this->sheetTitle;
}
public function array(): array
{
return $this->allRows;
}
/**
* Заголовки (с вложенной структурой)
*/
public function headings(): array
{
$profitTypeLabels = array_values($this->profitTypes);
$countTypes = count($profitTypeLabels);
$topRow = array_merge(
['Отделение', 'Поступило по типу оплаты'],
array_fill(0, max($countTypes - 1, 0), ''),
['Всего']
);
$subRow = array_merge([''], $profitTypeLabels, ['']);
return [
// Шапка отчета (первые 3 строки)
[$this->reportName],
['Дата создания: '.now()->format('d.m.Y H:i:s')],
[$this->formatDateRange()],
[], // Пустая строка для отступа
// Первый уровень заголовков (с объединением)
$topRow,
// Второй уровень заголовков (детализация по типам оплаты)
$subRow,
];
}
/**
* Форматирование дат для шапки
*/
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
{
$profitTypeIds = array_keys($this->profitTypes);
// Заголовок группы (название группы отделений / "Сводная информация")
if (isset($row['isGroupHeader']) && $row['isGroupHeader']) {
return array_merge([$row['groupName']], array_fill(0, count($profitTypeIds) + 1, ''));
}
// Обычное отделение и итоговые строки имеют одинаковую форму: department/counts/total
$counts = $row['counts'] ?? [];
$cells = [$row['department'] ?? ''];
foreach ($profitTypeIds as $profitTypeId) {
$cells[] = $this->formatZero($counts[$profitTypeId] ?? 0);
}
$cells[] = $this->formatZero($row['total'] ?? 0);
return $cells;
}
/**
* Стилизация 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);
$sheet->getPageSetup()->setFitToHeight(1);
$highestRow = $sheet->getHighestRow();
$countTypes = count($this->profitTypes);
$totalColumns = $countTypes + 2; // Отделение + типы оплаты + Всего
$highestColumn = Coordinate::stringFromColumnIndex($totalColumns);
$firstTypeColumn = Coordinate::stringFromColumnIndex(2); // B
$lastTypeColumn = Coordinate::stringFromColumnIndex(1 + $countTypes);
// ОБЪЕДИНЕНИЕ ЯЧЕЕК ДЛЯ ШАПКИ (строки 1-3)
$sheet->mergeCells('A1:'.$highestColumn.'1');
$sheet->mergeCells('A2:'.$highestColumn.'2');
$sheet->mergeCells('A3:'.$highestColumn.'3');
// ОБЪЕДИНЕНИЕ ДЛЯ ОБЫЧНЫХ ЗАГОЛОВКОВ (Отделение, Всего — по одной колонке на 2 строки)
$sheet->mergeCells('A5:A6');
$sheet->mergeCells($highestColumn.'5:'.$highestColumn.'6');
// ОБЪЕДИНЕНИЕ ДЛЯ ВЛОЖЕННОГО ЗАГОЛОВКА "Поступило по типу оплаты"
$sheet->mergeCells($firstTypeColumn.'5:'.$lastTypeColumn.'5');
$sheet->setCellValue($firstTypeColumn.'5', 'Поступило по типу оплаты');
// СТИЛИ ДЛЯ ШАПКИ ОТЧЕТА (строки 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'],
],
],
]);
// СТИЛИ ДЛЯ ВСЕХ СТРОК С ДАННЫМИ
$indexData = 0;
for ($row = 7; $row <= $highestRow; $row++) {
$currentRowInData = $this->allRows[$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->allRows) - 1) {
$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(30); // Отделение
for ($colIndex = 2; $colIndex <= $totalColumns; $colIndex++) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($colIndex))->setWidth(16);
}
// Увеличиваем высоту строк с заголовками
$sheet->getRowDimension(5)->setRowHeight(30);
$sheet->getRowDimension(6)->setRowHeight(25);
return [];
}
/**
* Форматирование нулевых значений
*/
protected function formatZero($value)
{
if (is_null($value) || $value === '' || $value === []) {
return '—';
}
if (is_array($value)) {
return '0';
}
if (is_numeric($value) && $value == 0) {
return '0';
}
return $value;
}
}