Compare commits

...

10 Commits

Author SHA1 Message Date
b0e7def24f
wip 2025-02-13 21:00:40 +01:00
0236f6f92a
wip 2025-02-13 21:00:40 +01:00
34d5145df4
revert 2025-02-13 21:00:40 +01:00
780c4841ac
revert 2025-02-13 21:00:40 +01:00
5427c557a1
add to address 2025-02-13 21:00:40 +01:00
316acf74d2
add ticket qr code 2025-02-13 21:00:40 +01:00
b33aad7c9f
wip 2025-02-13 21:00:40 +01:00
3870207471
add some stuff 2025-02-13 21:00:40 +01:00
5f68d12967
add success email 2025-02-13 21:00:40 +01:00
265ba6a8f5 style: update grid layout and improve form structure (#22)
Reviewed-on: #22
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
Co-authored-by: Jan-Marlon Leibl <jleibl@proton.me>
Co-committed-by: Jan-Marlon Leibl <jleibl@proton.me>
2025-02-13 19:57:04 +00:00
14 changed files with 347 additions and 27 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -7,6 +7,7 @@
"php": ">=8.2", "php": ">=8.2",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"chillerlan/php-qrcode": "^5.0",
"doctrine/annotations": "^2.0", "doctrine/annotations": "^2.0",
"doctrine/dbal": "^3", "doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-bundle": "^2.13",

161
composer.lock generated
View File

@ -4,8 +4,167 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "6e7803d436b49242a4a3816fdc776d3a", "content-hash": "26b10019eec6bd118285cb25e8daf878",
"packages": [ "packages": [
{
"name": "chillerlan/php-qrcode",
"version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/42e215640e9ebdd857570c9e4e52245d1ee51de2",
"reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^2.1.6 || ^3.2.1",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"chillerlan/php-authenticator": "^4.3.1 || ^5.2.1",
"ext-fileinfo": "*",
"phan/phan": "^5.4.5",
"phpcompatibility/php-compatibility": "10.x-dev",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"setasign/fpdf": "^1.8.2",
"slevomat/coding-standard": "^8.15",
"squizlabs/php_codesniffer": "^3.11"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT",
"Apache-2.0"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase/qrcode-generator"
},
{
"name": "ZXing Authors",
"homepage": "https://github.com/zxing/zxing"
},
{
"name": "Ashot Khanamiryan",
"homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"description": "A QR Code generator and reader with a user-friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"keywords": [
"phpqrcode",
"qr",
"qr code",
"qr-reader",
"qrcode",
"qrcode-generator",
"qrcode-reader"
],
"support": {
"docs": "https://php-qrcode.readthedocs.io",
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode"
},
"funding": [
{
"url": "https://ko-fi.com/codemasher",
"type": "Ko-Fi"
}
],
"time": "2024-11-21T16:12:34+00:00"
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.1"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"keywords": [
"Settings",
"configuration",
"container",
"helper"
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"time": "2024-07-16T11:13:48+00:00"
},
{ {
"name": "composer/semver", "name": "composer/semver",
"version": "3.4.3", "version": "3.4.3",

View File

@ -1,5 +1,7 @@
twig: twig:
file_name_pattern: '*.twig' file_name_pattern: '*.twig'
paths:
'%kernel.project_dir%/assets/images/email': images
when@test: when@test:
twig: twig:

View File

@ -0,0 +1,20 @@
<?php
namespace App\Controller;
use App\Service\TicketService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SuccessController extends AbstractController
{
#[Route(path: '/success', name: 'app_success_page', methods: Request::METHOD_GET)]
public function __invoke(TicketService $service, Request $request): Response
{
$service->completePayment((string)$request->query->get('session_id'));
return $this->render('ticket/success.html.twig');
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use chillerlan\QRCode\QRCode;
use Nucleos\DompdfBundle\Factory\DompdfFactory;
use Nucleos\DompdfBundle\Wrapper\DompdfWrapper;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class TestController extends AbstractController
{
#[Route('/test')]
public function index(Filesystem $filesystem): Response
{
$dompdfWrapper = new DompdfWrapper(new DompdfFactory());
$content = $dompdfWrapper->getPdf($this->renderView('test.html.twig', ['qr' => (new QRCode())->render('https://www.google.com')]));
$filesystem->dumpFile('test.pdf', $content);
return $this->render('test.html.twig', ['qr' => (new QRCode())->render('https://www.google.com')]);
}
}

View File

@ -33,7 +33,7 @@ final class TicketController extends AbstractController
return $this->json(['id' => $this->service->handleTicketData($ticketData)->id]); return $this->json(['id' => $this->service->handleTicketData($ticketData)->id]);
} }
#[Route(path: '/success', name: 'app_success', methods: Request::METHOD_GET)] #[Route(path: '/success', name: 'app_order_success', methods: Request::METHOD_GET)]
public function success(Request $request): Response public function success(Request $request): Response
{ {
$sessionId = $request->query->get('session_id'); $sessionId = $request->query->get('session_id');
@ -50,7 +50,7 @@ final class TicketController extends AbstractController
return $this->redirectToRoute('app_ticket'); return $this->redirectToRoute('app_ticket');
} }
return $this->render('ticket/success.html.twig'); return $this->redirectToRoute('app_success_page');
} }
#[Route(path: '/cancelled', name: 'app_cancelled', methods: Request::METHOD_GET)] #[Route(path: '/cancelled', name: 'app_cancelled', methods: Request::METHOD_GET)]

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Controller\Admin\CustomerCrudController;
use App\Entity\Payment;
use chillerlan\QRCode\QRCode;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class TicketEmailService
{
public function __construct(
private readonly MailerInterface $mailer,
#[Autowire(env: 'SENDER_MAIL')]
private readonly string $senderMail,
private readonly UrlGeneratorInterface $urlGenerator,
) {
}
public function sendSuccessEmail(Payment $payment): void
{
$mail = (new TemplatedEmail())
->htmlTemplate('email/order.html.twig')
->subject('Abiball Ticket')
->from(new Address($this->senderMail, 'Noreply'))
->to(new Address($payment->getCustomer()?->getEmail(), $payment->getCustomer()?->getFirstname() . ' ' . $payment->getCustomer()?->getLastname()))
->context([
'payment' => $payment,
'qr' => (new QRCode())->render($this->generateUrl($payment))
]);
$this->mailer->send($mail);
}
private function generateUrl(Payment $payment): string
{
return $this->urlGenerator->generate('admin', [
'crudAction' => 'detail',
'crudControllerFqcn' => CustomerCrudController::class,
'entityId' => $payment->getCustomer()?->getId()
], UrlGeneratorInterface::ABSOLUTE_URL);
}
}

View File

@ -21,6 +21,7 @@ class TicketService
private readonly UrlGeneratorInterface $generator, private readonly UrlGeneratorInterface $generator,
private readonly EntityManagerInterface $em, private readonly EntityManagerInterface $em,
private readonly PaymentRepository $paymentRepository, private readonly PaymentRepository $paymentRepository,
private readonly TicketEmailService $emailService,
#[Autowire(env: 'STRIPE_SECRET_KEY')] #[Autowire(env: 'STRIPE_SECRET_KEY')]
string $stripeKey string $stripeKey
) { ) {
@ -36,18 +37,6 @@ class TicketService
return $session; return $session;
} }
public function saveTicketData(TicketFormData $data, string $sessionId): void
{
$payment = (new Payment())
->setSessionId($sessionId)
->setCompleted(false)
->setCustomer($this->createEntityFromData($data));
$this->em->persist($payment);
$this->em->flush();
}
public function completePayment(string $sessionId): bool public function completePayment(string $sessionId): bool
{ {
if (!$payment = $this->paymentRepository->findOneBy(['sessionId' => $sessionId])) { if (!$payment = $this->paymentRepository->findOneBy(['sessionId' => $sessionId])) {
@ -57,9 +46,23 @@ class TicketService
$payment->setCompleted(true); $payment->setCompleted(true);
$this->em->flush(); $this->em->flush();
$this->emailService->sendSuccessEmail($payment);
return true; return true;
} }
private function saveTicketData(TicketFormData $data, string $sessionId): void
{
$payment = (new Payment())
->setSessionId($sessionId)
->setCompleted(false)
->setCustomer($this->createEntityFromData($data));
$this->em->persist($payment);
$this->em->flush();
}
private function getLineItems(array $tickets): array private function getLineItems(array $tickets): array
{ {
$lineItems = []; $lineItems = [];
@ -87,7 +90,7 @@ class TicketService
'line_items' => $lineItems, 'line_items' => $lineItems,
'mode' => 'payment', 'mode' => 'payment',
'customer_email' => $email, 'customer_email' => $email,
'success_url' => $this->generator->generate('app_success', [], 0) . '?session_id={CHECKOUT_SESSION_ID}', 'success_url' => $this->generator->generate('app_order_success', [], 0) . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $this->generator->generate('app_cancelled', [], 0), 'cancel_url' => $this->generator->generate('app_cancelled', [], 0),
]); ]);
} }

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abiball - Freie Waldorfschule Bremen</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f6f6f6;">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f6f6f6;">
<tr>
<td style="padding: 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" width="300" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
<tr>
<td style="background-color: #372064; padding: 10px 5px 5px; text-align: center;">
<img src="{{ email.image('@images/header.jpeg') }}" alt="Abiball" style="width: 290px; height: auto;">
</td>
</tr>
<tr>
<td style="padding: 0;">
<img src="{{ email.image('@images/background.jpeg') }}" alt="Celebration" style="width: 100%; max-width: 300px; height: auto; display: block;">
</td>
</tr>
<tr>
<td style="padding: 40px;">
<h2 style="text-align: center; color: #333333; font-size: 24px; margin-bottom: 20px;">You're Invited!</h2>
<p style="text-align: center; color: #666666; font-size: 16px; line-height: 1.5; margin-bottom: 30px;">
Join us for an evening of celebration, dancing, and memories at the Freie Waldorfschule Bremen Graduation Ball
</p>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f8f9fa; border-radius: 8px; margin-bottom: 30px;">
<tr>
<td style="padding: 25px;">
<h3 style="color: #333333; font-size: 18px; margin: 0 0 15px 0;">Event Details</h3>
<p style="color: #666666; margin: 5px 0;">Date: [Date]</p>
<p style="color: #666666; margin: 5px 0;">Time: [Time]</p>
<p style="color: #666666; margin: 5px 0;">Address: [Address]</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

13
templates/test.html.twig Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
<div class="bg-white/80 backdrop-blur-sm rounded-2xl border border-gray-100 p-5 hover:shadow-lg transition-all duration-300 group"> <div class="bg-white/80 backdrop-blur-sm rounded-2xl border border-gray-100 p-5 hover:shadow-lg transition-all duration-300 group">
<div class="grid grid-cols-12 gap-5"> <div class="grid grid-cols-1 md:grid-cols-12 gap-3 md:gap-5">
<div class="relative col-span-3"> <div class="md:col-span-3">
<div class="mb-1.5 text-gray-600 font-medium text-sm">Ticket-Typ</div>
<div class="relative group"> <div class="relative group">
<twig:ux:icon name="mingcute:ticket-fill" class="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-orange-400 pointer-events-none transition-colors group-hover:text-orange-500" /> <twig:ux:icon name="mingcute:ticket-fill" class="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-orange-400 pointer-events-none transition-colors group-hover:text-orange-500" />
<select name="ticket" <select name="ticket"
@ -19,7 +20,8 @@
</div> </div>
</div> </div>
<div class="relative col-span-3"> <div class="md:col-span-3">
<div class="mb-1.5 text-gray-600 font-medium text-sm">Ernährung</div>
<div class="relative group"> <div class="relative group">
<twig:ux:icon name="mdi:food" class="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-orange-400 pointer-events-none transition-colors group-hover:text-orange-500" /> <twig:ux:icon name="mdi:food" class="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-orange-400 pointer-events-none transition-colors group-hover:text-orange-500" />
<select name="food" <select name="food"
@ -37,7 +39,8 @@
</div> </div>
</div> </div>
<div class="relative col-span-5"> <div class="md:col-span-5">
<div class="mb-1.5 text-gray-600 font-medium text-sm">Anmerkungen</div>
<div class="relative group"> <div class="relative group">
<twig:ux:icon name="mdi:note" class="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-orange-400 pointer-events-none transition-colors group-hover:text-orange-500" /> <twig:ux:icon name="mdi:note" class="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-orange-400 pointer-events-none transition-colors group-hover:text-orange-500" />
<input type="text" <input type="text"
@ -48,7 +51,7 @@
</div> </div>
</div> </div>
<div class="flex justify-center items-end col-span-1 pb-1"> <div class="flex justify-end md:justify-center items-end md:col-span-1">
<button data-action="form#removeEntry" <button data-action="form#removeEntry"
type="button" type="button"
class="group/btn p-3 hover:bg-red-50 rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-red-200" class="group/btn p-3 hover:bg-red-50 rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-red-200"

View File

@ -60,12 +60,6 @@
<span>Tickets</span> <span>Tickets</span>
</h2> </h2>
<div class="ml-5 grid grid-cols-4 gap-4 mb-4 px-4 text-sm font-medium text-gray-600">
<div>Ticket</div>
<div>Ernährung</div>
<div>Anmerkungen</div>
</div>
<div class="forms space-y-4"> <div class="forms space-y-4">
{% include 'ticket/_partials/_form.html.twig' %} {% include 'ticket/_partials/_form.html.twig' %}
</div> </div>