feat: booking details pdf
This commit is contained in:
1 parent
d3666c4618
commit
2d7a02341d
19 files changed
+1054
-100
No files matched your search
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Email\PdfAttachment;
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Renders a one-page A4 booking document for a group accommodation booking.
|
||||
*
|
||||
* The document repeats the data of the admin detail page and includes the very same
|
||||
* price breakdown partial the booking emails use, so all three stay in sync.
|
||||
*/
|
||||
class AccommodationBookingPdfGenerator
|
||||
{
|
||||
private const TEMPLATE = 'pdf/accommodation_booking.html.twig';
|
||||
private const LOGO_PATH = '/assets/images/logo.png';
|
||||
|
||||
public function __construct(
|
||||
private readonly Environment $twig,
|
||||
private readonly AccommodationBookingBreakdownCalculator $breakdownCalculator,
|
||||
#[Autowire('%kernel.project_dir%')]
|
||||
private readonly string $projectDir,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException when the booking carries no price information
|
||||
*/
|
||||
public function render(AccommodationBooking $booking): string
|
||||
{
|
||||
$breakdown = $this->breakdownCalculator->compute($booking);
|
||||
|
||||
if (null === $breakdown) {
|
||||
throw new \RuntimeException('Für diese Buchung liegt keine Preisaufschlüsselung vor.');
|
||||
}
|
||||
|
||||
$html = $this->twig->render(self::TEMPLATE, [
|
||||
'booking' => $booking,
|
||||
'priceBreakdown' => $breakdown,
|
||||
'currency' => $booking->getPricingCurrency() ?? $breakdown['currency'],
|
||||
'logoSrc' => $this->logoDataUri(),
|
||||
]);
|
||||
|
||||
$options = new Options();
|
||||
// DejaVu Sans ships with dompdf and covers umlauts and the currency symbols
|
||||
// emitted by format_currency.
|
||||
$options->setDefaultFont('DejaVu Sans');
|
||||
$options->setDefaultPaperSize('A4');
|
||||
$options->setDefaultPaperOrientation('portrait');
|
||||
$options->setIsRemoteEnabled(false);
|
||||
|
||||
$dompdf = new Dompdf($options);
|
||||
$dompdf->loadHtml($html, 'UTF-8');
|
||||
$dompdf->render();
|
||||
|
||||
return (string) $dompdf->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException when the booking carries no price information
|
||||
*/
|
||||
public function createDownloadResponse(AccommodationBooking $booking): Response
|
||||
{
|
||||
$response = new Response($this->render($booking));
|
||||
$response->headers->set('Content-Type', 'application/pdf');
|
||||
$response->headers->set('Content-Disposition', $response->headers->makeDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$this->buildFilename($booking)
|
||||
));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Named after arrival date and group rather than the entity id, which is a local
|
||||
* detail with no counterpart in BusPro and would read as a booking number.
|
||||
*/
|
||||
/**
|
||||
* @throws \RuntimeException when the booking carries no price information
|
||||
*/
|
||||
public function createAttachment(AccommodationBooking $booking): PdfAttachment
|
||||
{
|
||||
return new PdfAttachment($this->render($booking), $this->buildFilename($booking));
|
||||
}
|
||||
|
||||
private function buildFilename(AccommodationBooking $booking): string
|
||||
{
|
||||
$groupName = (new AsciiSlugger('de'))->slug((string) $booking->getGroupName())->lower()->toString();
|
||||
|
||||
return sprintf(
|
||||
'buchung-%s%s.pdf',
|
||||
$booking->getDateFrom()?->format('Y-m-d') ?? 'ohne-datum',
|
||||
'' !== $groupName ? '-'.$groupName : ''
|
||||
);
|
||||
}
|
||||
|
||||
private function logoDataUri(): string
|
||||
{
|
||||
$logo = $this->projectDir.self::LOGO_PATH;
|
||||
|
||||
if (false === is_readable($logo)) {
|
||||
throw new \RuntimeException('Das Logo konnte nicht gelesen werden: '.self::LOGO_PATH);
|
||||
}
|
||||
|
||||
return 'data:image/png;base64,'.base64_encode((string) file_get_contents($logo));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user