|
- <?php
-
- declare(strict_types=1);
-
- namespace App\Services;
-
- use PhpOffice\PhpSpreadsheet\Spreadsheet;
- use PhpOffice\PhpSpreadsheet\Style\Fill;
- use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
- use Dompdf\Dompdf;
- use Dompdf\Options;
-
- class ExportService
- {
- /**
- * Build a ZIP archive containing XLSX + PDF for each territory.
- *
- * @param list<array<string,mixed>> $territories
- * @param list<array<string,mixed>> $allHouseholds All household rows (with territory_id)
- * @return string Raw ZIP binary content
- */
- public function buildZip(array $territories, array $allHouseholds): string
- {
- $byTerritory = [];
- foreach ($allHouseholds as $h) {
- $tid = (int) $h['territory_id'];
- $byTerritory[$tid][] = $h;
- }
-
- $zipFile = tempnam(sys_get_temp_dir(), 'territory_export_');
- $zip = new \ZipArchive();
- $zip->open($zipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
-
- foreach ($territories as $territory) {
- $tid = (int) $territory['id'];
- $households = $byTerritory[$tid] ?? [];
- $slug = $this->slug($territory['name']);
-
- $xlsx = $this->buildXlsx($territory, $households);
- $zip->addFromString("{$slug}.xlsx", $xlsx);
-
- $pdf = $this->buildPdf($territory, $households);
- $zip->addFromString("{$slug}.pdf", $pdf);
- }
-
- $zip->close();
- $content = (string) file_get_contents($zipFile);
- unlink($zipFile);
-
- return $content;
- }
-
- /** @param list<array<string,mixed>> $households */
- private function buildXlsx(array $territory, array $households): string
- {
- $spreadsheet = new Spreadsheet();
- $spreadsheet->removeSheetByIndex(0);
-
- $byStreet = $this->groupByStreet($households);
-
- if (empty($byStreet)) {
- $sheet = $spreadsheet->createSheet();
- $sheet->setTitle('No Data');
- $sheet->setCellValue('A1', 'No households found for this territory.');
- }
-
- foreach ($byStreet as $streetName => $streetHouseholds) {
- $sheet = $spreadsheet->createSheet();
- $sheet->setTitle(substr((string) $streetName, 0, 31));
-
- [$even, $odd] = $this->splitEvenOdd($streetHouseholds);
-
- $headerStyle = [
- 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
- 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '1D7A6D']],
- 'borders' => ['bottom' => ['borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN]],
- ];
-
- $evenHeaders = ['#', 'Address (Even)', 'Bus.', 'DNC', 'DNC Date'];
- $oddHeaders = ['#', 'Address (Odd)', 'Bus.', 'DNC', 'DNC Date'];
-
- foreach ($evenHeaders as $col => $header) {
- $cell = $this->colLetter($col + 1) . '1';
- $sheet->setCellValue($cell, $header);
- $sheet->getStyle($cell)->applyFromArray($headerStyle);
- }
-
- $sheet->setCellValue('G1', '');
-
- foreach ($oddHeaders as $col => $header) {
- $cell = $this->colLetter($col + 8) . '1';
- $sheet->setCellValue($cell, $header);
- $sheet->getStyle($cell)->applyFromArray($headerStyle);
- }
-
- $row = 2;
- foreach ($even as $h) {
- $sheet->setCellValue("A{$row}", $h['street_number'] ?? '');
- $sheet->setCellValue("B{$row}", $h['address'] ?? '');
- $sheet->setCellValue("C{$row}", $h['is_business'] ? 'Yes' : '');
- $sheet->setCellValue("D{$row}", $h['do_not_call'] ? 'Yes' : '');
- $sheet->setCellValue("E{$row}", $h['do_not_call_date'] ?? '');
- $row++;
- }
-
- $row = 2;
- foreach ($odd as $h) {
- $sheet->setCellValue("H{$row}", $h['street_number'] ?? '');
- $sheet->setCellValue("I{$row}", $h['address'] ?? '');
- $sheet->setCellValue("J{$row}", $h['is_business'] ? 'Yes' : '');
- $sheet->setCellValue("K{$row}", $h['do_not_call'] ? 'Yes' : '');
- $sheet->setCellValue("L{$row}", $h['do_not_call_date'] ?? '');
- $row++;
- }
-
- foreach (range('A', 'L') as $col) {
- $sheet->getColumnDimension($col)->setAutoSize(true);
- }
- }
-
- $writer = new Xlsx($spreadsheet);
- $tmpFile = tempnam(sys_get_temp_dir(), 'territory_xlsx_');
- $writer->save($tmpFile);
- $content = (string) file_get_contents($tmpFile);
- unlink($tmpFile);
-
- return $content;
- }
-
- /** @param list<array<string,mixed>> $households */
- private function buildPdf(array $territory, array $households): string
- {
- $options = new Options();
- $options->set('defaultFont', 'DejaVu Sans');
- $options->set('isRemoteEnabled', false);
-
- $dompdf = new Dompdf($options);
- $dompdf->loadHtml($this->buildPdfHtml($territory, $households));
- $dompdf->setPaper('A4', 'landscape');
- $dompdf->render();
-
- return (string) $dompdf->output();
- }
-
- /** @param list<array<string,mixed>> $households */
- private function buildPdfHtml(array $territory, array $households): string
- {
- $title = htmlspecialchars($territory['name'], ENT_QUOTES, 'UTF-8');
- $byStreet = $this->groupByStreet($households);
-
- $html = '<!DOCTYPE html><html><head><meta charset="UTF-8">
- <style>
- body { font-family: DejaVu Sans, sans-serif; font-size: 9pt; margin: 10mm; }
- h1 { font-size: 14pt; margin-bottom: 4px; }
- h2 { font-size: 11pt; margin: 12px 0 4px; border-bottom: 1px solid #555; padding-bottom: 2px; }
- table { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
- th { background: #1d7a6d; color: #fff; padding: 4px 6px; font-size: 8pt; text-align: left; }
- td { padding: 3px 6px; border-bottom: 1px solid #ddd; font-size: 8pt; vertical-align: top; }
- .dnc { color: #c0392b; font-weight: bold; }
- .sep { width: 6px; }
- </style>
- </head><body>';
-
- $html .= "<h1>Territory: {$title}</h1>";
-
- if (empty($byStreet)) {
- $html .= '<p>No households found.</p>';
- }
-
- foreach ($byStreet as $streetName => $streetHouseholds) {
- $escapedStreet = htmlspecialchars((string) $streetName, ENT_QUOTES, 'UTF-8');
- [$even, $odd] = $this->splitEvenOdd($streetHouseholds);
-
- $html .= "<h2>{$escapedStreet}</h2>";
- $html .= '<table><tr>
- <th>#</th><th>Address (Even)</th><th>DNC</th>
- <th class="sep"></th>
- <th>#</th><th>Address (Odd)</th><th>DNC</th>
- </tr>';
-
- $max = max(count($even), count($odd), 1);
- $evenVals = array_values($even);
- $oddVals = array_values($odd);
-
- for ($i = 0; $i < $max; $i++) {
- $html .= '<tr>';
- if (isset($evenVals[$i])) {
- $h = $evenVals[$i];
- $addr = htmlspecialchars($h['address'] ?? '', ENT_QUOTES, 'UTF-8');
- $dnc = $h['do_not_call'] ? '<span class="dnc">DNC</span>' : '';
- $html .= "<td>{$h['street_number']}</td><td>{$addr}</td><td>{$dnc}</td>";
- } else {
- $html .= '<td></td><td></td><td></td>';
- }
- $html .= '<td class="sep"></td>';
- if (isset($oddVals[$i])) {
- $h = $oddVals[$i];
- $addr = htmlspecialchars($h['address'] ?? '', ENT_QUOTES, 'UTF-8');
- $dnc = $h['do_not_call'] ? '<span class="dnc">DNC</span>' : '';
- $html .= "<td>{$h['street_number']}</td><td>{$addr}</td><td>{$dnc}</td>";
- } else {
- $html .= '<td></td><td></td><td></td>';
- }
- $html .= '</tr>';
- }
-
- $html .= '</table>';
- }
-
- $html .= '</body></html>';
- return $html;
- }
-
- /**
- * @param list<array<string,mixed>> $households
- * @return array<string, list<array<string,mixed>>>
- */
- private function groupByStreet(array $households): array
- {
- $byStreet = [];
- foreach ($households as $h) {
- $street = trim((string) ($h['street_name'] ?? ''));
- if ($street === '') {
- $street = 'Unknown Street';
- }
- $byStreet[$street][] = $h;
- }
-
- ksort($byStreet);
- return $byStreet;
- }
-
- /**
- * @param list<array<string,mixed>> $households
- * @return array{list<array<string,mixed>>, list<array<string,mixed>>}
- */
- private function splitEvenOdd(array $households): array
- {
- $even = [];
- $odd = [];
-
- foreach ($households as $h) {
- $num = (int) ($h['street_number'] ?? 0);
- if ($num % 2 === 0) {
- $even[] = $h;
- } else {
- $odd[] = $h;
- }
- }
-
- usort($even, fn($a, $b) => (int) ($a['street_number'] ?? 0) <=> (int) ($b['street_number'] ?? 0));
- usort($odd, fn($a, $b) => (int) ($a['street_number'] ?? 0) <=> (int) ($b['street_number'] ?? 0));
-
- return [$even, $odd];
- }
-
- private function colLetter(int $n): string
- {
- $letter = '';
- while ($n > 0) {
- $n--;
- $letter = chr(65 + ($n % 26)) . $letter;
- $n = (int) ($n / 26);
- }
- return $letter;
- }
-
- private function slug(string $name): string
- {
- $slug = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $name) ?? $name);
- return trim($slug, '_') ?: 'territory';
- }
- }
|