feat(tickets): add PDF and Excel export endpoints
This commit is contained in:
@@ -2,14 +2,23 @@
|
||||
|
||||
namespace App\Domains\Ticket\Controllers\AdminApp;
|
||||
|
||||
use App\Domains\Ticket\Requests\AdminAppTicketExportRequest;
|
||||
use App\Domains\Ticket\Requests\AdminAppTicketIndexRequest;
|
||||
use App\Domains\Ticket\Resources\AdminApp\AdminAppTicketCollection;
|
||||
use App\Domains\Ticket\Services\AdminAppTicketExcelService;
|
||||
use App\Domains\Ticket\Services\AdminAppTicketPdfService;
|
||||
use App\Domains\Ticket\Services\AdminAppTicketService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
public function __construct(private readonly AdminAppTicketService $ticketService) {}
|
||||
public function __construct(
|
||||
private readonly AdminAppTicketService $ticketService,
|
||||
private readonly AdminAppTicketPdfService $ticketPdfService,
|
||||
private readonly AdminAppTicketExcelService $ticketExcelService,
|
||||
) {}
|
||||
|
||||
public function index(AdminAppTicketIndexRequest $request): AdminAppTicketCollection
|
||||
{
|
||||
@@ -19,4 +28,26 @@ class TicketController extends Controller
|
||||
$this->ticketService->search($tenant, $request->validated())
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadPdf(AdminAppTicketExportRequest $request): Response
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return $this->ticketPdfService->download(
|
||||
$tenant,
|
||||
$this->ticketService->ticketsForExport($tenant, $request->validated()),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadExcel(AdminAppTicketExportRequest $request): StreamedResponse
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return $this->ticketExcelService->download(
|
||||
$tenant,
|
||||
$this->ticketService->ticketsForExport($tenant, $request->validated()),
|
||||
$request->validated('timezone'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
17
app/Domains/Ticket/Requests/AdminAppTicketExportRequest.php
Normal file
17
app/Domains/Ticket/Requests/AdminAppTicketExportRequest.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Requests;
|
||||
|
||||
use App\Domains\Shared\Rules\ValidTimezone;
|
||||
|
||||
class AdminAppTicketExportRequest extends AdminAppTicketIndexRequest
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
...parent::rules(),
|
||||
'timezone' => ['required', 'string', new ValidTimezone],
|
||||
];
|
||||
}
|
||||
}
|
||||
103
app/Domains/Ticket/Services/AdminAppTicketExcelService.php
Normal file
103
app/Domains/Ticket/Services/AdminAppTicketExcelService.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Services;
|
||||
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AdminAppTicketExcelService
|
||||
{
|
||||
public function __construct(private readonly AdminAppTicketReportService $reportService) {}
|
||||
|
||||
/** @param Collection<int, Ticket> $tickets */
|
||||
public function download(Tenant $tenant, Collection $tickets, string $timeZone): StreamedResponse
|
||||
{
|
||||
$generatedAt = now();
|
||||
$rows = $this->reportService->rows($tickets);
|
||||
$spreadsheet = new Spreadsheet;
|
||||
$spreadsheet->getProperties()
|
||||
->setCreator('Shopit')
|
||||
->setTitle('Listado de tickets')
|
||||
->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');
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
$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([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => '26382E'],
|
||||
],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$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);
|
||||
}
|
||||
|
||||
$filename = 'tickets_'.$tenant->codigo.'_'
|
||||
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx';
|
||||
|
||||
return response()->streamDownload(function () use ($spreadsheet): void {
|
||||
(new Xlsx($spreadsheet))->save('php://output');
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}, $filename, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
app/Domains/Ticket/Services/AdminAppTicketPdfService.php
Normal file
50
app/Domains/Ticket/Services/AdminAppTicketPdfService.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Services;
|
||||
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Barryvdh\DomPDF\PDF as DomPdf;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AdminAppTicketPdfService
|
||||
{
|
||||
public function __construct(private readonly AdminAppTicketReportService $reportService) {}
|
||||
|
||||
/** @param Collection<int, Ticket> $tickets */
|
||||
public function download(Tenant $tenant, Collection $tickets, string $timeZone): Response
|
||||
{
|
||||
$generatedAt = now();
|
||||
$pdf = Pdf::loadView('pdf.adminapp.tickets', [
|
||||
'tenant' => $tenant,
|
||||
'tickets' => $this->reportService->rows($tickets),
|
||||
'generatedAt' => $generatedAt,
|
||||
'timeZone' => $timeZone,
|
||||
])->setPaper('a3', 'landscape');
|
||||
|
||||
$this->addPageNumbers($pdf);
|
||||
|
||||
return $pdf->download(
|
||||
'tickets_'.$tenant->codigo.'_'
|
||||
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
private function addPageNumbers(DomPdf $pdf): void
|
||||
{
|
||||
$pdf->render();
|
||||
$domPdf = $pdf->getDomPDF();
|
||||
$font = $domPdf->getFontMetrics()->getFont('DejaVu Sans');
|
||||
|
||||
$domPdf->getCanvas()->page_text(
|
||||
565,
|
||||
805,
|
||||
'Página {PAGE_NUM} de {PAGE_COUNT}',
|
||||
$font,
|
||||
7,
|
||||
[0.48, 0.52, 0.49],
|
||||
);
|
||||
}
|
||||
}
|
||||
112
app/Domains/Ticket/Services/AdminAppTicketReportService.php
Normal file
112
app/Domains/Ticket/Services/AdminAppTicketReportService.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Services;
|
||||
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use App\Domains\Ticket\Resources\AdminApp\AdminAppTicketResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AdminAppTicketReportService
|
||||
{
|
||||
private const CATEGORY_PRESENTATIONS = [
|
||||
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null],
|
||||
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null],
|
||||
'entradas' => ['category' => null, 'product' => 'product', 'type' => null],
|
||||
'comidas' => ['category' => 'Comida', 'product' => 'event_date', 'type' => 'horario'],
|
||||
'comida' => ['category' => null, 'product' => 'event_date', 'type' => 'horario'],
|
||||
'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Collection<int, Ticket> $tickets
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
*/
|
||||
public function rows(Collection $tickets): Collection
|
||||
{
|
||||
return $tickets->values()->map(fn (Ticket $ticket): array => $this->row($ticket));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function row(Ticket $ticket): array
|
||||
{
|
||||
$data = (new AdminAppTicketResource($ticket))->resolve();
|
||||
$presentation = $this->presentation($data);
|
||||
|
||||
return [
|
||||
'order_number' => $data['order_number'],
|
||||
'category' => $presentation['category'],
|
||||
'product' => $presentation['product'],
|
||||
'type' => $presentation['type'],
|
||||
'amount' => $data['amount'] === null ? null : (float) $data['amount'],
|
||||
'client' => $data['client'] ?? 'Sin nombre',
|
||||
'ticket' => $data['ticket'],
|
||||
'date' => $data['date'],
|
||||
'status' => $this->statusLabel($data['status']),
|
||||
'scanned_by' => $data['scanned_by'] ?? '-',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $ticket
|
||||
* @return array{category: string, product: string, type: string}
|
||||
*/
|
||||
private function presentation(array $ticket): array
|
||||
{
|
||||
$sourceCategory = trim((string) ($ticket['category'] ?? '')) ?: '-';
|
||||
$configuration = self::CATEGORY_PRESENTATIONS[mb_strtolower($sourceCategory)] ?? null;
|
||||
|
||||
if ($configuration === null) {
|
||||
return [
|
||||
'category' => $sourceCategory,
|
||||
'product' => (string) ($ticket['product'] ?: $ticket['name'] ?: '-'),
|
||||
'type' => $this->allPropertyLabels($ticket) ?: '-',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'category' => $configuration['category'] ?? $sourceCategory,
|
||||
'product' => $configuration['product'] === 'product'
|
||||
? (string) ($ticket['product'] ?: $ticket['name'] ?: '-')
|
||||
: ($this->propertyLabels($ticket, $configuration['product']) ?: '-'),
|
||||
'type' => $configuration['type'] === null
|
||||
? '-'
|
||||
: ($this->propertyLabels($ticket, $configuration['type']) ?: '-'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $ticket */
|
||||
private function propertyLabels(array $ticket, string $code): string
|
||||
{
|
||||
$property = collect($ticket['variant_properties'] ?? [])->firstWhere('code', $code);
|
||||
$labels = collect($property['values'] ?? [])->pluck('label')->filter();
|
||||
|
||||
if ($code === 'event_date') {
|
||||
$labels = $labels->map(function (string $label): string {
|
||||
[$day, $month] = array_pad(explode('/', $label), 2, null);
|
||||
|
||||
return $day !== null && $month !== null ? "{$day}/{$month}" : $label;
|
||||
});
|
||||
}
|
||||
|
||||
return $labels->implode(', ');
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $ticket */
|
||||
private function allPropertyLabels(array $ticket): string
|
||||
{
|
||||
return collect($ticket['variant_properties'] ?? [])
|
||||
->flatMap(fn (array $property): array => $property['values'] ?? [])
|
||||
->pluck('label')
|
||||
->filter()
|
||||
->implode(', ');
|
||||
}
|
||||
|
||||
private function statusLabel(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
Ticket::STATUS_USED => 'Usado',
|
||||
Ticket::STATUS_EXPIRED => 'Vencido',
|
||||
default => 'Activo',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,10 @@ Route::prefix('v1/adminapp/tenant')
|
||||
Route::get('tickets', [TicketController::class, 'index'])
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
->name('adminapp.tickets.index');
|
||||
Route::get('tickets/pdf', [TicketController::class, 'downloadPdf'])
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
->name('adminapp.tickets.pdf');
|
||||
Route::get('tickets/excel', [TicketController::class, 'downloadExcel'])
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
->name('adminapp.tickets.excel');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user