From 6a00d8d09cd8ed89233a45831c81790853082c38 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 31 Aug 2026 12:31:32 -0300 Subject: [PATCH] refactor(tickets): reuse dynamic columns in exports --- .../Services/AdminAppTicketExcelService.php | 88 ++++++++++--------- .../Services/AdminAppTicketPdfService.php | 10 ++- .../views/pdf/adminapp/tickets.blade.php | 44 +++------- .../AdminAppTicketExportServiceTest.php | 35 +++++--- 4 files changed, 90 insertions(+), 87 deletions(-) diff --git a/app/Domains/Ticket/Services/AdminAppTicketExcelService.php b/app/Domains/Ticket/Services/AdminAppTicketExcelService.php index 50a6517..c34496e 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketExcelService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketExcelService.php @@ -6,6 +6,7 @@ use App\Domains\Tenant\Models\Tenant; use App\Domains\Ticket\Models\Ticket; use Carbon\CarbonInterface; use Illuminate\Support\Collection; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -16,13 +17,17 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class AdminAppTicketExcelService { - public function __construct(private readonly AdminAppTicketReportService $reportService) {} + public function __construct( + private readonly AdminAppTicketReportService $reportService, + private readonly AdminAppTicketColumnService $columnService, + ) {} /** @param Collection $tickets */ public function download(Tenant $tenant, Collection $tickets, string $timeZone): StreamedResponse { $generatedAt = now(); $rows = $this->reportService->rows($tickets); + $columns = $this->columnService->columns($tenant); $spreadsheet = new Spreadsheet; $spreadsheet->getProperties() ->setCreator('Shopit') @@ -30,48 +35,52 @@ class AdminAppTicketExcelService ->setSubject($tenant->nombre); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Tickets'); - $sheet->fromArray([ - 'N° de orden', - 'Categoría', - 'Producto', - 'Tipo', - 'Importe', - 'Cliente', - 'ID', - 'Fecha', - 'Estado', - 'Escaneado por', - ], null, 'A1'); + $sheet->fromArray([array_column($columns, 'label')], null, 'A1'); foreach ($rows as $index => $ticket) { $row = $index + 2; - $sheet->setCellValueExplicit( - "A{$row}", - $ticket['order_number'] === null ? '-' : '#'.$ticket['order_number'], - DataType::TYPE_STRING, - ); - $sheet->setCellValueExplicit("B{$row}", $ticket['category'], DataType::TYPE_STRING); - $sheet->setCellValueExplicit("C{$row}", $ticket['product'], DataType::TYPE_STRING); - $sheet->setCellValueExplicit("D{$row}", $ticket['type'], DataType::TYPE_STRING); - if ($ticket['amount'] !== null) { - $sheet->setCellValue("E{$row}", $ticket['amount']); - } - $sheet->setCellValueExplicit("F{$row}", $ticket['client'], DataType::TYPE_STRING); - $sheet->setCellValueExplicit("G{$row}", $ticket['ticket'], DataType::TYPE_STRING); - if ($ticket['date'] instanceof CarbonInterface) { - $sheet->setCellValue( - "H{$row}", - Date::dateTimeToExcel($ticket['date']->copy()->timezone($timeZone)), + foreach ($columns as $columnIndex => $column) { + $coordinate = Coordinate::stringFromColumnIndex($columnIndex + 1).$row; + $value = $ticket[$column['key']] ?? null; + + if ($column['type'] === 'currency' && $value !== null) { + $sheet->setCellValue($coordinate, (float) $value); + + continue; + } + + if ($column['type'] === 'date' && $value instanceof CarbonInterface) { + $sheet->setCellValue( + $coordinate, + Date::dateTimeToExcel($value->copy()->timezone($timeZone)), + ); + + continue; + } + + $sheet->setCellValueExplicit( + $coordinate, + $this->reportService->displayValue($value, $column['type'], $timeZone), + DataType::TYPE_STRING, ); } - $sheet->setCellValueExplicit("I{$row}", $ticket['status'], DataType::TYPE_STRING); - $sheet->setCellValueExplicit("J{$row}", $ticket['scanned_by'], DataType::TYPE_STRING); } $lastRow = max(2, $rows->count() + 1); - $sheet->getStyle("E2:E{$lastRow}")->getNumberFormat()->setFormatCode('$ #,##0.00'); - $sheet->getStyle("H2:H{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm'); - $sheet->getStyle('A1:J1')->applyFromArray([ + $lastColumn = Coordinate::stringFromColumnIndex(count($columns)); + foreach ($columns as $columnIndex => $column) { + $letter = Coordinate::stringFromColumnIndex($columnIndex + 1); + if ($column['type'] === 'currency') { + $sheet->getStyle("{$letter}2:{$letter}{$lastRow}") + ->getNumberFormat()->setFormatCode('$ #,##0.00'); + } + if ($column['type'] === 'date') { + $sheet->getStyle("{$letter}2:{$letter}{$lastRow}") + ->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm'); + } + $sheet->getColumnDimension($letter)->setWidth($column['excel_width']); + } + $sheet->getStyle("A1:{$lastColumn}1")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], 'fill' => [ 'fillType' => Fill::FILL_SOLID, @@ -81,14 +90,7 @@ class AdminAppTicketExcelService ]); $sheet->getRowDimension(1)->setRowHeight(24); $sheet->freezePane('A2'); - $sheet->setAutoFilter("A1:J{$lastRow}"); - - foreach ([ - 'A' => 14, 'B' => 18, 'C' => 22, 'D' => 18, 'E' => 15, - 'F' => 30, 'G' => 39, 'H' => 20, 'I' => 13, 'J' => 28, - ] as $column => $width) { - $sheet->getColumnDimension($column)->setWidth($width); - } + $sheet->setAutoFilter("A1:{$lastColumn}{$lastRow}"); $filename = 'tickets_'.$tenant->codigo.'_' .$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx'; diff --git a/app/Domains/Ticket/Services/AdminAppTicketPdfService.php b/app/Domains/Ticket/Services/AdminAppTicketPdfService.php index d6a92ef..83e85ac 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketPdfService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketPdfService.php @@ -11,15 +11,21 @@ use Illuminate\Support\Collection; class AdminAppTicketPdfService { - public function __construct(private readonly AdminAppTicketReportService $reportService) {} + public function __construct( + private readonly AdminAppTicketReportService $reportService, + private readonly AdminAppTicketColumnService $columnService, + ) {} /** @param Collection $tickets */ public function download(Tenant $tenant, Collection $tickets, string $timeZone): Response { $generatedAt = now(); + $columns = $this->columnService->columns($tenant); + $rows = $this->reportService->rows($tickets); $pdf = Pdf::loadView('pdf.adminapp.tickets', [ 'tenant' => $tenant, - 'tickets' => $this->reportService->rows($tickets), + 'columns' => $columns, + 'tickets' => $this->reportService->displayRows($rows, $columns, $timeZone), 'generatedAt' => $generatedAt, 'timeZone' => $timeZone, ])->setPaper('a3', 'landscape'); diff --git a/resources/views/pdf/adminapp/tickets.blade.php b/resources/views/pdf/adminapp/tickets.blade.php index 0ce5bdd..2baa4af 100644 --- a/resources/views/pdf/adminapp/tickets.blade.php +++ b/resources/views/pdf/adminapp/tickets.blade.php @@ -18,16 +18,6 @@ .number { text-align: right; } .ticket-id { font-size: 6.8px; word-break: break-all; } .empty { color: #66736b; padding: 24px; text-align: center; } - .order { width: 7%; } - .category { width: 9%; } - .product { width: 10%; } - .type { width: 9%; } - .amount { width: 8%; } - .client { width: 14%; } - .identifier { width: 17%; } - .date { width: 10%; } - .status { width: 7%; } - .scanner { width: 9%; } @@ -43,34 +33,26 @@ - - - - - - - - - - + @foreach ($columns as $column) + + @endforeach @forelse ($tickets as $ticket) - - - - - - - - - - + @foreach ($columns as $column) + + @endforeach @empty - + @endforelse
N° de ordenCategoríaProductoTipoImporteClienteIDFechaEstadoEscaneado por $column['type'] === 'currency']) + >{{ $column['label'] }}
{{ $ticket['order_number'] === null ? '-' : '#'.$ticket['order_number'] }}{{ $ticket['category'] }}{{ $ticket['product'] }}{{ $ticket['type'] }}{{ $ticket['amount'] === null ? '-' : '$'.number_format($ticket['amount'], 2, ',', '.') }}{{ $ticket['client'] }}{{ $ticket['ticket'] }}{{ $ticket['date']?->copy()->timezone($timeZone)->format('d/m/Y H:i') ?? '-' }}{{ $ticket['status'] }}{{ $ticket['scanned_by'] }} $column['type'] === 'currency', + 'ticket-id' => $column['key'] === 'ticket', + ])>{{ $ticket[$column['key']] }}
No hay tickets para los criterios seleccionados.
No hay tickets para los criterios seleccionados.
diff --git a/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php b/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php index c238222..33e674a 100644 --- a/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php +++ b/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php @@ -3,9 +3,11 @@ namespace Tests\Unit\Ticket; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Services\AdminAppTicketColumnService; use App\Domains\Ticket\Services\AdminAppTicketExcelService; use App\Domains\Ticket\Services\AdminAppTicketPdfService; use App\Domains\Ticket\Services\AdminAppTicketReportService; +use App\Domains\Ticket\Services\AdminAppTicketRowService; use Barryvdh\DomPDF\ServiceProvider; use Illuminate\Support\Carbon; use Mockery; @@ -32,7 +34,7 @@ class AdminAppTicketExportServiceTest extends TestCase public function test_it_downloads_the_ticket_report_as_an_excel_file(): void { - $response = (new AdminAppTicketExcelService($this->reportService())) + $response = (new AdminAppTicketExcelService($this->reportService(), $this->columnService())) ->download($this->tenant(), collect(), 'America/La_Paz'); $this->assertSame( @@ -40,7 +42,7 @@ class AdminAppTicketExportServiceTest extends TestCase $response->headers->get('content-type'), ); $this->assertStringContainsString( - 'attachment; filename=tickets_acme_20260824_135300.xlsx', + 'attachment; filename=tickets_fiesta_futbol_infantil_20260824_135300.xlsx', (string) $response->headers->get('content-disposition'), ); @@ -62,12 +64,12 @@ class AdminAppTicketExportServiceTest extends TestCase public function test_it_downloads_the_ticket_report_as_a_pdf(): void { - $response = (new AdminAppTicketPdfService($this->reportService())) + $response = (new AdminAppTicketPdfService($this->reportService(), $this->columnService())) ->download($this->tenant(), collect(), 'America/La_Paz'); $this->assertSame('application/pdf', $response->headers->get('content-type')); $this->assertStringContainsString( - 'attachment; filename=tickets_acme_20260824_135300.pdf', + 'attachment; filename=tickets_fiesta_futbol_infantil_20260824_135300.pdf', (string) $response->headers->get('content-disposition'), ); $this->assertStringStartsWith('%PDF', $response->getContent()); @@ -75,9 +77,15 @@ class AdminAppTicketExportServiceTest extends TestCase public function test_the_pdf_view_contains_the_report_data_and_requested_timezone(): void { + $columns = $this->columnService()->columns($this->tenant()); $html = view('pdf.adminapp.tickets', [ 'tenant' => $this->tenant(), - 'tickets' => collect([$this->row()]), + 'columns' => $columns, + 'tickets' => (new AdminAppTicketRowService)->displayRows( + collect([$this->row()]), + $columns, + 'America/La_Paz', + ), 'generatedAt' => now(), 'timeZone' => 'America/La_Paz', ])->render(); @@ -90,10 +98,10 @@ class AdminAppTicketExportServiceTest extends TestCase private function reportService(): AdminAppTicketReportService { - $service = Mockery::mock(AdminAppTicketReportService::class); - $service->shouldReceive('rows')->once()->andReturn(collect([$this->row()])); + $rowService = Mockery::mock(AdminAppTicketRowService::class)->makePartial(); + $rowService->shouldReceive('rows')->once()->andReturn(collect([$this->row()])); - return $service; + return new AdminAppTicketReportService($rowService); } /** @return array */ @@ -108,7 +116,7 @@ class AdminAppTicketExportServiceTest extends TestCase 'client' => 'Cliente Test', 'ticket' => '00000000-0000-0000-0000-000000000001', 'date' => now(), - 'status' => 'Usado', + 'status' => 'used', 'scanned_by' => 'Admin Test', ]; } @@ -116,11 +124,16 @@ class AdminAppTicketExportServiceTest extends TestCase private function tenant(): Tenant { return (new Tenant)->forceFill([ - 'codigo' => 'acme', - 'nombre' => 'Acme Eventos', + 'codigo' => 'fiesta_futbol_infantil', + 'nombre' => 'Fiesta del Fútbol Infantil', ]); } + private function columnService(): AdminAppTicketColumnService + { + return new AdminAppTicketColumnService; + } + private function spreadsheetPath(StreamedResponse $response): string { ob_start();