refactor(tickets): reuse dynamic columns in exports
This commit is contained in:
@@ -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<int, Ticket> $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';
|
||||
|
||||
@@ -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<int, Ticket> $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');
|
||||
|
||||
@@ -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%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -43,34 +33,26 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="order">N° de orden</th>
|
||||
<th class="category">Categoría</th>
|
||||
<th class="product">Producto</th>
|
||||
<th class="type">Tipo</th>
|
||||
<th class="amount number">Importe</th>
|
||||
<th class="client">Cliente</th>
|
||||
<th class="identifier">ID</th>
|
||||
<th class="date">Fecha</th>
|
||||
<th class="status">Estado</th>
|
||||
<th class="scanner">Escaneado por</th>
|
||||
@foreach ($columns as $column)
|
||||
<th
|
||||
style="width: {{ $column['width'] }}"
|
||||
@class(['number' => $column['type'] === 'currency'])
|
||||
>{{ $column['label'] }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($tickets as $ticket)
|
||||
<tr>
|
||||
<td>{{ $ticket['order_number'] === null ? '-' : '#'.$ticket['order_number'] }}</td>
|
||||
<td>{{ $ticket['category'] }}</td>
|
||||
<td>{{ $ticket['product'] }}</td>
|
||||
<td>{{ $ticket['type'] }}</td>
|
||||
<td class="number">{{ $ticket['amount'] === null ? '-' : '$'.number_format($ticket['amount'], 2, ',', '.') }}</td>
|
||||
<td>{{ $ticket['client'] }}</td>
|
||||
<td class="ticket-id">{{ $ticket['ticket'] }}</td>
|
||||
<td>{{ $ticket['date']?->copy()->timezone($timeZone)->format('d/m/Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $ticket['status'] }}</td>
|
||||
<td>{{ $ticket['scanned_by'] }}</td>
|
||||
@foreach ($columns as $column)
|
||||
<td @class([
|
||||
'number' => $column['type'] === 'currency',
|
||||
'ticket-id' => $column['key'] === 'ticket',
|
||||
])>{{ $ticket[$column['key']] }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td class="empty" colspan="10">No hay tickets para los criterios seleccionados.</td></tr>
|
||||
<tr><td class="empty" colspan="{{ count($columns) }}">No hay tickets para los criterios seleccionados.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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<string, mixed> */
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user