feat(desfile): export entry reservations

This commit is contained in:
2026-09-24 10:14:56 -03:00
parent d8d8354070
commit 0e870776c4
9 changed files with 375 additions and 7 deletions

View File

@@ -2,12 +2,17 @@
namespace App\Domains\Ticketing\Desfile\Controllers;
use App\Domains\Ticketing\Desfile\Requests\ExportEntryReservationsRequest;
use App\Domains\Ticketing\Desfile\Requests\IndexEntryReservationsRequest;
use App\Domains\Ticketing\Desfile\Requests\StoreEntryReservationsRequest;
use App\Domains\Ticketing\Desfile\Resources\EntryReservationResource;
use App\Domains\Ticketing\Desfile\Services\EntryReservationExcelService;
use App\Domains\Ticketing\Desfile\Services\EntryReservationPdfService;
use App\Domains\Ticketing\Desfile\Services\EntryReservationService;
use App\Http\Controllers\Controller;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class EntryReservationController extends Controller
{
@@ -18,6 +23,34 @@ class EntryReservationController extends Controller
);
}
public function downloadPdf(
ExportEntryReservationsRequest $request,
EntryReservationService $service,
EntryReservationPdfService $pdf,
): Response {
$user = $request->user();
return $pdf->download(
$user->tenant()->firstOrFail(),
$service->reservationsForExport($user, $request->validated()),
$request->validated('timezone'),
);
}
public function downloadExcel(
ExportEntryReservationsRequest $request,
EntryReservationService $service,
EntryReservationExcelService $excel,
): StreamedResponse {
$user = $request->user();
return $excel->download(
$user->tenant()->firstOrFail(),
$service->reservationsForExport($user, $request->validated()),
$request->validated('timezone'),
);
}
public function store(StoreEntryReservationsRequest $request, EntryReservationService $service): AnonymousResourceCollection
{
return EntryReservationResource::collection($service->reserve(

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Ticketing\Desfile\Requests;
use App\Shared\Rules\ValidTimezone;
class ExportEntryReservationsRequest extends IndexEntryReservationsRequest
{
public function rules(): array
{
return [
...parent::rules(),
'timezone' => ['required', 'string', new ValidTimezone],
];
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Domains\Ticketing\Desfile\Services;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
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 EntryReservationExcelService
{
public function __construct(private readonly EntryReservationReportService $report) {}
/** @param Collection<int, EntryReservation> $reservations */
public function download(Tenant $tenant, Collection $reservations, string $timeZone): StreamedResponse
{
$generatedAt = now();
$rows = $this->report->rows($reservations);
$spreadsheet = new Spreadsheet;
$spreadsheet->getProperties()
->setCreator('Shopit')
->setTitle('Reservas de entradas')
->setSubject($tenant->nombre);
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Reservas');
$sheet->fromArray([
'Tipo',
'Sector',
'Fila',
'Asiento',
'ID',
'Fecha',
'Importe',
'Pago',
], null, 'A1');
foreach ($rows as $index => $reservation) {
$row = $index + 2;
foreach (['A' => 'tipo', 'B' => 'sector', 'C' => 'fila', 'D' => 'asiento'] as $column => $key) {
$sheet->setCellValueExplicit("{$column}{$row}", $reservation[$key], DataType::TYPE_STRING);
}
$sheet->setCellValueExplicit(
"E{$row}",
$reservation['ticket_id'] === null ? '-' : (string) $reservation['ticket_id'],
DataType::TYPE_STRING,
);
$sheet->setCellValue(
"F{$row}",
Date::dateTimeToExcel($reservation['fecha_reserva']->copy()->timezone($timeZone)),
);
if ($reservation['importe'] !== null) {
$sheet->setCellValue("G{$row}", (float) $reservation['importe']);
}
$sheet->setCellValueExplicit("H{$row}", $reservation['pago'], DataType::TYPE_STRING);
}
$lastRow = max(2, $rows->count() + 1);
$sheet->getStyle("F2:F{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm');
$sheet->getStyle("G2:G{$lastRow}")->getNumberFormat()->setFormatCode('$ #,##0.00');
$sheet->getStyle('A1:H1')->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:H{$lastRow}");
foreach (['A' => 20, 'B' => 22, 'C' => 12, 'D' => 12, 'E' => 16, 'F' => 20, 'G' => 16, 'H' => 18] as $column => $width) {
$sheet->getColumnDimension($column)->setWidth($width);
}
$filename = 'reservas_entradas_'.$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',
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Domains\Ticketing\Desfile\Services;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
use Barryvdh\DomPDF\Facade\Pdf;
use Barryvdh\DomPDF\PDF as DomPdf;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class EntryReservationPdfService
{
public function __construct(private readonly EntryReservationReportService $report) {}
/** @param Collection<int, EntryReservation> $reservations */
public function download(Tenant $tenant, Collection $reservations, string $timeZone): Response
{
$generatedAt = now();
$rows = $this->report->rows($reservations);
$pdf = Pdf::loadView('pdf.adminapp.desfile-entry-reservations', [
'tenant' => $tenant,
'reservations' => $rows,
'generatedAt' => $generatedAt,
'timeZone' => $timeZone,
])->setPaper('a4', 'landscape');
$this->addPageNumbers($pdf);
return $pdf->download(
'reservas_entradas_'.$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(
385,
575,
'Página {PAGE_NUM} de {PAGE_COUNT}',
$font,
7,
[0.48, 0.52, 0.49],
);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Domains\Ticketing\Desfile\Services;
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
use Illuminate\Support\Collection;
class EntryReservationReportService
{
/**
* @param Collection<int, EntryReservation> $reservations
* @return Collection<int, array<string, mixed>>
*/
public function rows(Collection $reservations): Collection
{
return $reservations->values()->map(function (EntryReservation $reservation): array {
$selection = $reservation->variant->selectionOptions();
return [
'tipo' => $this->selectionLabel($selection->get('tipo')),
'sector' => $this->selectionLabel($selection->get('sector')),
'fila' => $this->selectionLabel($selection->get('fila')),
'asiento' => $this->selectionLabel($selection->get('asiento')),
'ticket_id' => $reservation->ticket_id,
'fecha_reserva' => $reservation->fecha_reserva,
'importe' => $reservation->importe,
'pago' => $reservation->tipo_pago->label(),
];
});
}
private function selectionLabel(mixed $selection): string
{
if (! is_array($selection)) {
return '-';
}
if (array_is_list($selection)) {
return collect($selection)->pluck('label')->filter()->implode(', ') ?: '-';
}
return isset($selection['label']) ? (string) $selection['label'] : '-';
}
}

View File

@@ -29,6 +29,29 @@ class EntryReservationService
{
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
return $this->reservationsQuery($user, $filters)
->paginate(
perPage: $filters['per_page'] ?? 15,
pageName: 'page',
page: $filters['page'] ?? 1,
)
->withQueryString();
}
/**
* @param array{tipo_pago?: string|null} $filters
* @return Collection<int, EntryReservation>
*/
public function reservationsForExport(User $user, array $filters = []): Collection
{
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
return $this->reservationsQuery($user, $filters)->get();
}
/** @param array{tipo_pago?: string|null} $filters */
private function reservationsQuery(User $user, array $filters = []): Builder
{
return EntryReservation::query()
->whereHas('variant.catalogItem', fn (Builder $query): Builder => $query
->where('tenant_code', $user->tenant_codigo)
@@ -44,13 +67,7 @@ class EntryReservationService
'variant.eventDate',
])
->orderByDesc('fecha_reserva')
->orderByDesc('id')
->paginate(
perPage: $filters['per_page'] ?? 15,
pageName: 'page',
page: $filters['page'] ?? 1,
)
->withQueryString();
->orderByDesc('id');
}
/** @param list<array{variant_id: int, tipo_pago: string}> $rows */

View File

@@ -7,6 +7,12 @@ use Illuminate\Support\Facades\Route;
Route::get('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'index'])
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
->name('adminapp.desfile.entry-reservations.index');
Route::get('v1/adminapp/tenant/desfile/entry-reservations/pdf', [EntryReservationController::class, 'downloadPdf'])
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
->name('adminapp.desfile.entry-reservations.pdf');
Route::get('v1/adminapp/tenant/desfile/entry-reservations/excel', [EntryReservationController::class, 'downloadExcel'])
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
->name('adminapp.desfile.entry-reservations.excel');
Route::post('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'store'])
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
->name('adminapp.desfile.entry-reservations.store');