Squashed commit of the following:

commit c993da3397b65488d919d1b929cbd4638754601b
Author: ncoronel <ncoronel@quo.ar>
Date:   Tue Jul 28 09:54:10 2026 -0300

    feat(tickets): update ticket metadata layout for improved readability and clarity

commit 140fa4676d84bed788894e1d7994b26dccac7a34
Author: ncoronel <ncoronel@quo.ar>
Date:   Tue Jul 28 09:43:21 2026 -0300

    feat(tickets): enhance PDF ticket download with dynamic filename and improved styling

commit 8875c047b198776e61e8353c99cf4c7b00bc8fc9
Author: ncoronel <ncoronel@quo.ar>
Date:   Tue Jul 28 09:03:17 2026 -0300

    feat(tickets): add PDF download functionality for tickets

    - Implemented a new endpoint to download tickets as a PDF.
    - Created DownloadTicketsPdfRequest for validating ticket IDs.
    - Added TicketPdfService to handle PDF generation and QR code embedding.
    - Developed a Blade view for rendering ticket details in PDF format.
    - Updated API routes to include the new PDF download route.
    - Added tests to ensure authenticated users can download their tickets and that users cannot download tickets belonging to others.
    - Updated composer.json and composer.lock to include necessary dependencies for PDF generation and QR code creation.
This commit is contained in:
2026-07-28 09:54:49 -03:00
parent 5107d858cc
commit 754aeebe3a
8 changed files with 1087 additions and 1 deletions

View File

@@ -4,13 +4,18 @@ namespace App\Domains\Ticket\Controllers;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket;
use App\Domains\Ticket\Requests\DownloadTicketsPdfRequest;
use App\Domains\Ticket\Resources\TicketResource;
use App\Domains\Ticket\Services\TicketPdfService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class TicketController extends Controller
{
public function __construct(private readonly TicketPdfService $ticketPdfService) {}
public function index(Request $request, Tenant $tenant): JsonResponse
{
$tickets = Ticket::query()
@@ -21,4 +26,23 @@ class TicketController extends Controller
return TicketResource::collection($tickets)->response();
}
public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response
{
$ticketIds = $request->validated('ticket_ids');
$tickets = Ticket::query()
->where('tenant_code', $tenant->codigo)
->where('user_id', $request->user()->getKey())
->whereIn('id', $ticketIds)
->orderByDesc('id')
->get();
abort_if(
$tickets->count() !== count($ticketIds),
404,
'Uno o más tickets no están disponibles.'
);
return $this->ticketPdfService->download($tenant, $tickets);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Domains\Ticket\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DownloadTicketsPdfRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, array<int, string>> */
public function rules(): array
{
return [
'ticket_ids' => ['required', 'array', 'min:1', 'max:25'],
'ticket_ids.*' => ['required', 'integer', 'distinct'],
];
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Domains\Ticket\Services;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket;
use Barryvdh\DomPDF\Facade\Pdf;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Throwable;
class TicketPdfService
{
/**
* @param Collection<int, Ticket> $tickets
*/
public function download(Tenant $tenant, Collection $tickets): Response
{
$tenant->loadMissing('headerLogo');
$primaryColor = $this->color($tenant->primary_color, '#009933');
$headerBackgroundColor = $this->color($tenant->header_bg_color, $primaryColor);
$pdf = Pdf::loadView('pdf.tickets', [
'tenant' => $tenant,
'tickets' => $tickets,
'logoDataUri' => $this->logoDataUri($tenant),
'primaryColor' => $primaryColor,
'headerBackgroundColor' => $headerBackgroundColor,
'headerTextColor' => $this->contrastingTextColor($headerBackgroundColor),
'qrCodes' => $tickets->mapWithKeys(
fn (Ticket $ticket): array => [$ticket->id => $this->qrCodeDataUri($ticket->ticket)]
),
])->setPaper('a4');
$ticketIds = $tickets->pluck('id')->implode('_');
return $pdf->download("tickets_{$ticketIds}.pdf");
}
private function qrCodeDataUri(string $value): string
{
$qrCode = new QrCode(
data: $value,
errorCorrectionLevel: ErrorCorrectionLevel::Medium,
size: 700,
margin: 10,
);
return (new PngWriter)->write($qrCode)->getDataUri();
}
private function logoDataUri(Tenant $tenant): ?string
{
$logo = $tenant->headerLogo;
if ($logo === null) {
return null;
}
try {
$contents = Storage::disk('s3')->get($logo->path);
} catch (Throwable) {
return null;
}
return 'data:'.($logo->mime_type ?: 'image/png').';base64,'.base64_encode($contents);
}
private function color(?string $color, string $fallback): string
{
return is_string($color) && preg_match('/^#[0-9A-Fa-f]{6}$/', $color)
? $color
: $fallback;
}
private function contrastingTextColor(string $backgroundColor): string
{
$red = hexdec(substr($backgroundColor, 1, 2));
$green = hexdec(substr($backgroundColor, 3, 2));
$blue = hexdec(substr($backgroundColor, 5, 2));
$luminance = ($red * 299 + $green * 587 + $blue * 114) / 1000;
return $luminance > 160 ? '#17211b' : '#ffffff';
}
}

View File

@@ -7,4 +7,5 @@ Route::prefix('tenants/{tenant:codigo}')
->middleware('auth:sanctum')
->group(function (): void {
Route::get('tickets', [TicketController::class, 'index']);
Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']);
});