Jelajahi Sumber

schedule export: split long pdf output into pages

Alexander Musikhin 2 minggu lalu
induk
melakukan
2d97181384

+ 113 - 59
app/Services/ExportScheduleService.php

@@ -14,10 +14,14 @@ use PhpOffice\PhpSpreadsheet\Style\Border;
 use PhpOffice\PhpSpreadsheet\Style\Color;
 use PhpOffice\PhpSpreadsheet\Style\Fill;
 use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
 use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 
 class ExportScheduleService
 {
+    private const DATA_START_ROW = 3;
+    private const DATA_END_COLUMN = 'K';
+    private const PDF_ROWS_PER_PAGE = 24;
 
 
     /**
@@ -25,89 +29,139 @@ class ExportScheduleService
      */
     public function handle(Collection $schedules, int $userId): string
     {
+        $spreadsheet = $this->buildSpreadsheet($schedules);
+        $sheet = $spreadsheet->getActiveSheet();
+        $header = (string) $sheet->getCell('A1')->getValue();
+        $fileName = $header . '.xlsx';
+
+        $writer = new Xlsx($spreadsheet);
+        $fd = 'export/schedule/tmp';
+        Storage::disk('public')->makeDirectory($fd);
+        $fp = storage_path('app/public/export/schedule/') . '/tmp/' . $fileName;
+        Storage::disk('public')->delete($fd . '/' . $fileName);
+        $writer->save($fp);
+        PdfConverterClient::convert($fp);
+
+        // create zip archive
+        $fileModel = (new FileService())->createZipArchive($fd, Str::replace('.xlsx', '.zip', $fileName), $userId);
+
+        //   remove temp files
+        Storage::disk('public')->deleteDirectory($fd);
+
+        // return link
+        return $fileModel?->link ?? '';
 
-        $inputFileType = 'Xlsx'; // Xlsx - Xml - Ods - Slk - Gnumeric - Csv
-        $inputFileName = './templates/Schedule.xlsx';
+    }
 
-        $reader = IOFactory::createReader($inputFileType);
-        $spreadsheet = $reader->load($inputFileName);
+    public function buildSpreadsheet(Collection $schedules): \PhpOffice\PhpSpreadsheet\Spreadsheet
+    {
+        $reader = IOFactory::createReader('Xlsx');
+        $spreadsheet = $reader->load('./templates/Schedule.xlsx');
         $sheet = $spreadsheet->getActiveSheet();
         $sheet->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
 
-        $i = 3;
-        $first = true;
+        $row = self::DATA_START_ROW;
         $from = '-';
-        $prevInstDate = '';
-        $j = 1;
+        $prevInstDate = null;
+        $sameDateRows = 0;
+        $rowsOnCurrentPage = 0;
+        $lastScheduleDate = null;
+
         foreach ($schedules as $schedule) {
-            if($first) {
-                $first = false;
+            if ($prevInstDate === null) {
                 $from = $schedule->installation_date;
                 $prevInstDate = $schedule->installation_date;
-
-            } elseif($prevInstDate === $schedule->installation_date) {
-                $j++;
+                $sameDateRows = 1;
+            } elseif ($prevInstDate === $schedule->installation_date) {
+                $sameDateRows++;
             } else {
-                if($j > 1) {
-                    $sheet->mergeCells('A' . $i - $j . ':A' . $i - 1);
-                    $sheet->mergeCells('B' . $i - $j . ':B' . $i - 1);
+                if ($sameDateRows > 1) {
+                    $this->mergeDateColumns($sheet, $row - $sameDateRows, $row - 1);
+                }
+
+                if ($rowsOnCurrentPage >= self::PDF_ROWS_PER_PAGE) {
+                    $sheet->setBreak('A' . $row, Worksheet::BREAK_ROW);
+                    $rowsOnCurrentPage = 0;
                 }
-                $j = 1;
+
+                $sameDateRows = 1;
                 $prevInstDate = $schedule->installation_date;
             }
 
-            $sheet->setCellValue('A' . $i, DateHelper::getHumanDayOfWeek($schedule->installation_date));
-            $sheet->setCellValue('B' . $i, DateHelper::getHumanDate($schedule->installation_date, true));
-
-            $sheet->setCellValue('C' . $i, $schedule->address_code);
-            $sheet->setCellValue('D' . $i, $schedule->district->shortname);
-            $sheet->setCellValue('E' . $i, $schedule->area->name);
-            $sheet->setCellValue('F' . $i, $schedule->object_address);
-            $sheet->setCellValue('G' . $i, $schedule->object_type);
-            $sheet->setCellValue('H' . $i, Str::trim($schedule->mafs));
-            $sheet->setCellValue('I' . $i, $schedule->mafs_count);
-            $sheet->setCellValue('J' . $i, $schedule->brigadier->name);
-            $sheet->setCellValue('K' . $i, $schedule->comment);
-
-            $sheet->getStyle('C' . $i . ':K' . $i)->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB(Str::replace('#', '', $schedule->brigadier->color));
-            $i++;
+            $sheet->setCellValue('A' . $row, DateHelper::getHumanDayOfWeek($schedule->installation_date));
+            $sheet->setCellValue('B' . $row, DateHelper::getHumanDate($schedule->installation_date, true));
+
+            $sheet->setCellValue('C' . $row, $schedule->address_code);
+            $sheet->setCellValue('D' . $row, $schedule->district->shortname);
+            $sheet->setCellValue('E' . $row, $schedule->area->name);
+            $sheet->setCellValue('F' . $row, $schedule->object_address);
+            $sheet->setCellValue('G' . $row, $schedule->object_type);
+            $sheet->setCellValue('H' . $row, Str::trim($schedule->mafs));
+            $sheet->setCellValue('I' . $row, $schedule->mafs_count);
+            $sheet->setCellValue('J' . $row, $schedule->brigadier->name);
+            $sheet->setCellValue('K' . $row, $schedule->comment);
+
+            $sheet->getStyle('C' . $row . ':K' . $row)
+                ->getFill()
+                ->setFillType(Fill::FILL_SOLID)
+                ->getStartColor()
+                ->setRGB(Str::replace('#', '', $schedule->brigadier->color));
+
+            $lastScheduleDate = $schedule->installation_date;
+            $row++;
+            $rowsOnCurrentPage++;
         }
-        // merge last cells if $j > 1
-        if($j > 1) {
-            $sheet->mergeCells('A' . $i - $j . ':A' . $i - 1);
-            $sheet->mergeCells('B' . $i - $j . ':B' . $i - 1);
+
+        if ($sameDateRows > 1) {
+            $this->mergeDateColumns($sheet, $row - $sameDateRows, $row - 1);
         }
 
-        $sheet->getStyle('A2:B' . $i -1)->getAlignment()->setTextRotation(90);
+        $lastDataRow = max($row - 1, 2);
 
-        $sheet->getStyle('A2:K' . $i - 1)->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN)->setColor(new Color('777777'));
-        $sheet->getStyle('A2:K' . $i - 1)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
-        $sheet->getStyle('C2:K' . $i - 1)->getAlignment()->setWrapText(true);
+        $sheet->getStyle('A2:B' . $lastDataRow)->getAlignment()->setTextRotation(90);
+        $sheet->getStyle('A2:' . self::DATA_END_COLUMN . $lastDataRow)
+            ->getBorders()
+            ->getAllBorders()
+            ->setBorderStyle(Border::BORDER_THIN)
+            ->setColor(new Color('777777'));
+        $sheet->getStyle('A2:' . self::DATA_END_COLUMN . $lastDataRow)
+            ->getAlignment()
+            ->setHorizontal(Alignment::HORIZONTAL_CENTER);
+        $sheet->getStyle('C2:' . self::DATA_END_COLUMN . $lastDataRow)->getAlignment()->setWrapText(true);
 
-        $fromText = (DateHelper::isDate($from)) ? DateHelper::getHumanDate($from) : '-';
-        $toText = (isset($schedule) && $schedule->installation_date) ? DateHelper::getHumanDate($schedule->installation_date) : '-';
+        $this->configurePdfLayout($sheet, $lastDataRow);
+
+        $fromText = DateHelper::isDate($from) ? DateHelper::getHumanDate($from) : '-';
+        $toText = DateHelper::isDate((string) $lastScheduleDate) ? DateHelper::getHumanDate($lastScheduleDate) : '-';
         $header = 'График монтажей с ' . $fromText . ' по ' . $toText;
 
         $sheet->setCellValue('A1', $header);
         $sheet->getStyle('A1')->getFont()->setBold(true);
-        $fileName = $header . '.xlsx';
-
-        $writer = new Xlsx($spreadsheet);
-        $fd = 'export/schedule/tmp';
-        Storage::disk('public')->makeDirectory($fd);
-        $fp = storage_path('app/public/export/schedule/') . '/tmp/' . $fileName;
-        Storage::disk('public')->delete($fd . '/' . $fileName);
-        $writer->save($fp);
-        PdfConverterClient::convert($fp);
-
-        // create zip archive
-        $fileModel = (new FileService())->createZipArchive($fd, Str::replace('.xlsx', '.zip', $fileName), $userId);
 
-        //   remove temp files
-        Storage::disk('public')->deleteDirectory($fd);
+        return $spreadsheet;
+    }
 
-        // return link
-        return $fileModel?->link ?? '';
+    private function mergeDateColumns(Worksheet $sheet, int $startRow, int $endRow): void
+    {
+        $sheet->mergeCells('A' . $startRow . ':A' . $endRow);
+        $sheet->mergeCells('B' . $startRow . ':B' . $endRow);
+    }
 
+    private function configurePdfLayout(Worksheet $sheet, int $lastDataRow): void
+    {
+        $pageSetup = $sheet->getPageSetup();
+        $pageSetup->setPaperSize(PageSetup::PAPERSIZE_A4);
+        $pageSetup->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
+        $pageSetup->setFitToWidth(1);
+        $pageSetup->setFitToHeight(0);
+
+        $sheet->getPageMargins()
+            ->setTop(0.4)
+            ->setBottom(0.4)
+            ->setLeft(0.25)
+            ->setRight(0.25);
+
+        $pageSetup->setRowsToRepeatAtTopByStartAndEnd(1, 2);
+        $pageSetup->setPrintArea('A1:' . self::DATA_END_COLUMN . $lastDataRow);
     }
-}
+}

+ 72 - 0
tests/Unit/Services/Export/ExportScheduleServiceTest.php

@@ -7,8 +7,10 @@ use App\Models\Dictionary\District;
 use App\Models\Schedule;
 use App\Models\User;
 use App\Services\ExportScheduleService;
+use Illuminate\Database\Eloquent\Factories\Sequence;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Support\Collection;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
 use Tests\TestCase;
 
 class ExportScheduleServiceTest extends TestCase
@@ -144,4 +146,74 @@ class ExportScheduleServiceTest extends TestCase
             $this->assertTrue(true);
         }
     }
+
+    public function test_build_spreadsheet_sets_pdf_layout_for_multiple_pages(): void
+    {
+        if (!file_exists('./templates/Schedule.xlsx')) {
+            $this->markTestSkipped('Excel template Schedule.xlsx not found');
+        }
+
+        $brigadier = User::factory()->brigadier()->create([
+            'color' => '#123456',
+        ]);
+
+        $district = District::query()->inRandomOrder()->first()
+            ?? District::factory()->create();
+
+        $area = Area::query()->inRandomOrder()->first()
+            ?? Area::factory()->create(['district_id' => $district->id]);
+
+        $schedules = Schedule::factory()
+            ->count(30)
+            ->sequence(fn (Sequence $sequence) => [
+                'brigadier_id' => $brigadier->id,
+                'district_id' => $district->id,
+                'area_id' => $area->id,
+                'installation_date' => now()->addDays($sequence->index)->format('Y-m-d'),
+            ])
+            ->create()
+            ->load(['district', 'area', 'brigadier']);
+
+        $spreadsheet = $this->service->buildSpreadsheet(Collection::make($schedules->all()));
+        $sheet = $spreadsheet->getActiveSheet();
+
+        $this->assertSame('A1:K32', $sheet->getPageSetup()->getPrintArea());
+        $this->assertSame(1, $sheet->getPageSetup()->getFitToWidth());
+        $this->assertSame(0, $sheet->getPageSetup()->getFitToHeight());
+        $this->assertSame(['A27' => Worksheet::BREAK_ROW], $sheet->getBreaks());
+    }
+
+    public function test_build_spreadsheet_keeps_small_schedule_on_single_page(): void
+    {
+        if (!file_exists('./templates/Schedule.xlsx')) {
+            $this->markTestSkipped('Excel template Schedule.xlsx not found');
+        }
+
+        $brigadier = User::factory()->brigadier()->create([
+            'color' => '#654321',
+        ]);
+
+        $district = District::query()->inRandomOrder()->first()
+            ?? District::factory()->create();
+
+        $area = Area::query()->inRandomOrder()->first()
+            ?? Area::factory()->create(['district_id' => $district->id]);
+
+        $schedules = Schedule::factory()
+            ->count(2)
+            ->sequence(fn (Sequence $sequence) => [
+                'brigadier_id' => $brigadier->id,
+                'district_id' => $district->id,
+                'area_id' => $area->id,
+                'installation_date' => now()->addDays($sequence->index)->format('Y-m-d'),
+            ])
+            ->create()
+            ->load(['district', 'area', 'brigadier']);
+
+        $spreadsheet = $this->service->buildSpreadsheet(Collection::make($schedules->all()));
+        $sheet = $spreadsheet->getActiveSheet();
+
+        $this->assertSame('A1:K4', $sheet->getPageSetup()->getPrintArea());
+        $this->assertSame([], $sheet->getBreaks());
+    }
 }