From 03e01c5b58e5c36a95662381641166c1a6d1778b Mon Sep 17 00:00:00 2001 From: Jerlio <43434126+jerlio@users.noreply.github.com> Date: Thu, 4 Jul 2024 12:51:23 +0200 Subject: [PATCH] QR Code Sponsor : nouvelle page et ajout d'un scan par form (#1492) --- app/Resources/translations/messages.en.yml | 11 +- .../views/event/sponsor/base.html.twig | 7 + .../views/event/sponsor/scan.html.twig | 29 ++++ .../views/event/sponsor/scan_new.html.twig | 9 ++ .../views/event/ticket/sponsor.html.twig | 7 + app/config/routing/event.yml | 12 ++ db/migrations/20240422143329_qr_code.php | 28 ++++ htdocs/css/sponsor_scan.css | 8 ++ .../Controller/SponsorScanController.php | 134 ++++++++++++++++++ .../AppBundle/Event/Form/SponsorScanType.php | 28 ++++ .../Repository/SponsorScanRepository.php | 77 ++++++++++ .../Model/Repository/TicketRepository.php | 5 + sources/AppBundle/Event/Model/SponsorScan.php | 133 +++++++++++++++++ sources/AppBundle/Event/Model/Ticket.php | 25 ++++ 14 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 app/Resources/views/event/sponsor/base.html.twig create mode 100644 app/Resources/views/event/sponsor/scan.html.twig create mode 100644 app/Resources/views/event/sponsor/scan_new.html.twig create mode 100644 db/migrations/20240422143329_qr_code.php create mode 100644 htdocs/css/sponsor_scan.css create mode 100644 sources/AppBundle/Controller/SponsorScanController.php create mode 100644 sources/AppBundle/Event/Form/SponsorScanType.php create mode 100644 sources/AppBundle/Event/Model/Repository/SponsorScanRepository.php create mode 100644 sources/AppBundle/Event/Model/SponsorScan.php diff --git a/app/Resources/translations/messages.en.yml b/app/Resources/translations/messages.en.yml index 9c8a55591..e0ad97c5a 100644 --- a/app/Resources/translations/messages.en.yml +++ b/app/Resources/translations/messages.en.yml @@ -172,4 +172,13 @@ speaker_from_previous_event: "We filled the form with the profile you used on a "Personne référente": "Liaison" "sera votre personne de contact privilégiée dans les semaines menant à l’évènement tout comme durant l’évènement. N’hésitez pas à prendre contact pour toutes questions relatives à votre venue.": "will be your main contact person in the weeks leading up to the event as well as during the event. Please do not hesitate to contact us if you have any question about your visit." "Plusieurs évènements en ce moment !": "Several events at the moment!" -"Évènements": "Events" \ No newline at end of file +"Évènements": "Events" +"Espace sponsor": "Sponsor space" +"Billetterie sponsors": "Sponsor tickets" +"QR Codes": "QR Codes" +"SCANNEUR DE QR CODES": "QR Codes Scanner" +"Scanner un QR Code": "Scan a QR Code" +"QR Codes scannés": "Scanned QR Codes" +"Télécharger en CSV": "Download CSV" +"Supprimer": "Delete" +"Lancer l'outil de scan de QR Codes": "Run QR Code Scanner" \ No newline at end of file diff --git a/app/Resources/views/event/sponsor/base.html.twig b/app/Resources/views/event/sponsor/base.html.twig new file mode 100644 index 000000000..b51c5cd6b --- /dev/null +++ b/app/Resources/views/event/sponsor/base.html.twig @@ -0,0 +1,7 @@ +{% extends ':event:base.html.twig' %} + +{% block title %}{{ 'Espace sponsor'|trans }} - Afup{% endblock %} + +{% if menu is not defined %} + {% set menu = {'main': {'route' : 'sponsor_ticket_form', 'title': 'Billetterie sponsors'}} %} +{% endif %} diff --git a/app/Resources/views/event/sponsor/scan.html.twig b/app/Resources/views/event/sponsor/scan.html.twig new file mode 100644 index 000000000..77a373078 --- /dev/null +++ b/app/Resources/views/event/sponsor/scan.html.twig @@ -0,0 +1,29 @@ +{% extends ':event/sponsor:base.html.twig' %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block content %} +
+
+

{{ 'QR Codes'|trans }} : {{ event.title }} - {{ sponsorTicket.company }}

+

{% trans %}SCANNEUR DE QR CODES{% endtrans %}

+ + +

{% trans %}QR CODES SCANNÉS{% endtrans %}

+ {% trans %}Télécharger en CSV{% endtrans %} + +
+ {% for scan in scans %} +
{{ scan.prenom }} {{ scan.nom }}
+
+ {{ scan.email }}
+ {{ scan.created_on|date('d/m/Y H:i')}} - {% trans %}Supprimer{% endtrans %} +
+ {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/Resources/views/event/sponsor/scan_new.html.twig b/app/Resources/views/event/sponsor/scan_new.html.twig new file mode 100644 index 000000000..0244d3320 --- /dev/null +++ b/app/Resources/views/event/sponsor/scan_new.html.twig @@ -0,0 +1,9 @@ +{% extends ':event/sponsor:base.html.twig' %} + +{% block content %} +
+
+ {{ form(form) }} +
+
+{% endblock %} \ No newline at end of file diff --git a/app/Resources/views/event/ticket/sponsor.html.twig b/app/Resources/views/event/ticket/sponsor.html.twig index 3c1ac085c..7368490cf 100644 --- a/app/Resources/views/event/ticket/sponsor.html.twig +++ b/app/Resources/views/event/ticket/sponsor.html.twig @@ -10,6 +10,13 @@

{{ 'Billetterie sponsor'|trans }}: {{ event.title }} - {{ sponsorTicket.company }}

+ {% if sponsorTicket.qrCodesScannerAvailable %} +

{% trans %}SCANNEUR DE QR CODES{% endtrans %}

+ + {% trans %}Lancer l'outil de scan de QR Codes{% endtrans %} + + {% endif %} +

{% trans %}VOS PLACES GRATUITES{% endtrans %}

{% if sold_out %} diff --git a/app/config/routing/event.yml b/app/config/routing/event.yml index 3c43759ed..8bd4a9bfc 100644 --- a/app/config/routing/event.yml +++ b/app/config/routing/event.yml @@ -100,6 +100,18 @@ sponsor_leads_post: path: /{eventSlug}/sponsor/thank-you defaults: {_controller: AppBundle:Lead:postLead} +sponsor_scan: + path: /{eventSlug}/sponsor/scan + defaults: {_controller: AppBundle:SponsorScan:index} + +sponsor_scan_new: + path: /{eventSlug}/sponsor/scan/new + defaults: {_controller: AppBundle:SponsorScan:new} + +sponsor_scan_flash: + path: /{eventSlug}/sponsor/scan/flash/{code} + defaults: {_controller: AppBundle:SponsorScan:flash} + speaker-infos: path: /{eventSlug}/speaker-infos defaults: {_controller: AppBundle\Controller\Event\SpeakerPageAction } diff --git a/db/migrations/20240422143329_qr_code.php b/db/migrations/20240422143329_qr_code.php new file mode 100644 index 000000000..2bfd5ae16 --- /dev/null +++ b/db/migrations/20240422143329_qr_code.php @@ -0,0 +1,28 @@ +execute($sql); + + $sql = <<execute($sql); + } +} diff --git a/htdocs/css/sponsor_scan.css b/htdocs/css/sponsor_scan.css new file mode 100644 index 000000000..1e79c2d00 --- /dev/null +++ b/htdocs/css/sponsor_scan.css @@ -0,0 +1,8 @@ +/* en mobile */ +@media (max-width: 460px) { + .button.sponsor_scan { + display: block; + margin: 0 auto; + width: 80%; + } +} \ No newline at end of file diff --git a/sources/AppBundle/Controller/SponsorScanController.php b/sources/AppBundle/Controller/SponsorScanController.php new file mode 100644 index 000000000..f343d4119 --- /dev/null +++ b/sources/AppBundle/Controller/SponsorScanController.php @@ -0,0 +1,134 @@ +checkEventSlug($eventSlug); + + try { + $sponsorTicket = $this->checkSponsorTicket($request); + } catch (\Exception $e) { + $this->addFlash('error', $e->getMessage()); + return $this->redirectToRoute('sponsor_ticket_home', ['eventSlug' => $eventSlug]); + } + + /** @var SponsorScanRepository $scanRepository */ + $scanRepository = $this->get('ting')->get(SponsorScanRepository::class); + $scans = $scanRepository->getBySponsorTicket($sponsorTicket); + + return $this->render(':event/sponsor:scan.html.twig', [ + 'event' => $event, + 'sponsorTicket' => $sponsorTicket, + 'scans' => $scans, + ]); + } + + public function newAction(Request $request, $eventSlug) + { + $event = $this->checkEventSlug($eventSlug); + + try { + $this->checkSponsorTicket($request); + } catch (\Exception $e) { + $this->addFlash('error', $e->getMessage()); + return $this->redirectToRoute('sponsor_ticket_home', ['eventSlug' => $eventSlug]); + } + + $form = $this->createForm(SponsorScanType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + return $this->redirectToRoute('sponsor_scan_flash', [ + 'eventSlug' => $event->getPath(), + 'code' => $data['code'], + ]); + } + + return $this->render(':event/sponsor:scan_new.html.twig', [ + 'event' => $event, + 'form' => $form->createView(), + ]); + } + + public function flashAction(Request $request, string $code, string $eventSlug) + { + $event = $this->checkEventSlug($eventSlug); + + try { + $sponsorTicket = $this->checkSponsorTicket($request); + } catch (\Exception $e) { + $this->addFlash('error', $e->getMessage()); + return $this->redirectToRoute('sponsor_ticket_home', ['eventSlug' => $eventSlug]); + } + + /** @var TicketRepository $ticketRepository */ + $ticketRepository = $this->get('ting')->get(TicketRepository::class); + /** @var Ticket $ticket */ + $ticket = $ticketRepository->getOneBy(['forumId' => $event->getId(), 'qrCode' => $code]); + + if ($ticket === null) { + $this->addFlash('error', 'Code inexistant ou invalide'); + return $this->redirectToRoute('sponsor_scan', ['eventSlug' => $eventSlug]); + } + + /** @var SponsorScanRepository $scanRepository */ + $scanRepository = $this->get('ting')->get(SponsorScanRepository::class); + $scan = $scanRepository->getOneBy(['sponsorTicketId' => $sponsorTicket->getId(), 'ticketId' => $ticket->getId()]); + + if ($scan instanceof SponsorScan && $scan->getDeletedOn() === null) { + $this->addFlash('error', 'Code déjà scanné.'); + return $this->redirectToRoute('sponsor_scan', ['eventSlug' => $eventSlug]); + } + + if (!$scan instanceof SponsorScan) { + $scan = (new SponsorScan()) + ->setSponsorTicketId($sponsorTicket->getId()) + ->setTicketId($ticket->getId()); + } + + $scan->setCreatedOn(new \DateTime('now')) + ->setDeletedOn(null); + $scanRepository->save($scan); + + $this->addFlash('success', 'QR Code ajouté !'); + + return $this->redirectToRoute('sponsor_scan', ['eventSlug' => $eventSlug]); + } + + private function checkSponsorTicket(Request $request) + { + if ($request->getSession()->has('sponsor_ticket_id') === false) { + throw new \Exception('Merci de renseigner votre token.'); + } + + /** + * @var SponsorTicket $sponsorTicket + */ + $sponsorTicket = $this->get('ting')->get(SponsorTicketRepository::class)->get($request->getSession()->get('sponsor_ticket_id')); + if ($sponsorTicket === null) { + throw new \Exception('Token invalide.'); + } + + if (!$sponsorTicket->getQrCodesScannerAvailable()) { + throw new \Exception('Accès non autorisé.'); + } + + return $sponsorTicket; + } +} diff --git a/sources/AppBundle/Event/Form/SponsorScanType.php b/sources/AppBundle/Event/Form/SponsorScanType.php new file mode 100644 index 000000000..3a338a66a --- /dev/null +++ b/sources/AppBundle/Event/Form/SponsorScanType.php @@ -0,0 +1,28 @@ +add('code', TextType::class, [ + 'required' => true, + 'constraints' => [ + new NotBlank(), + ], + 'label' => 'Code', + ]) + ->add('save', SubmitType::class, ['label' => 'Valider']) + ; + } +} diff --git a/sources/AppBundle/Event/Model/Repository/SponsorScanRepository.php b/sources/AppBundle/Event/Model/Repository/SponsorScanRepository.php new file mode 100644 index 000000000..f7769abb9 --- /dev/null +++ b/sources/AppBundle/Event/Model/Repository/SponsorScanRepository.php @@ -0,0 +1,77 @@ +getPreparedQuery($sql) + ->setParams(['sponsorTicketId' => $sponsorTicket->getId()]) + ->query($this->getCollection(new HydratorArray())); + } + + /** + * @inheritDoc + */ + public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) + { + $metadata = new Metadata($serializerFactory); + + $metadata->setEntity(SponsorScan::class); + $metadata->setConnectionName('main'); + $metadata->setDatabase($options['database']); + $metadata->setTable('afup_forum_sponsor_scan'); + + $metadata + ->addField([ + 'columnName' => 'id', + 'fieldName' => 'id', + 'primary' => true, + 'autoincrement' => true, + 'type' => 'int' + ]) + ->addField([ + 'columnName' => 'sponsor_ticket_id', + 'fieldName' => 'sponsorTicketId', + 'type' => 'string' + ]) + ->addField([ + 'columnName' => 'ticket_id', + 'fieldName' => 'ticketId', + 'type' => 'int' + ]) + ->addField([ + 'columnName' => 'created_on', + 'fieldName' => 'createdOn', + 'type' => 'datetime' + ]) + ->addField([ + 'columnName' => 'deleted_on', + 'fieldName' => 'deletedOn', + 'type' => 'datetime' + ]) + ; + + return $metadata; + } +} diff --git a/sources/AppBundle/Event/Model/Repository/TicketRepository.php b/sources/AppBundle/Event/Model/Repository/TicketRepository.php index 50855c5e0..f212be1b8 100644 --- a/sources/AppBundle/Event/Model/Repository/TicketRepository.php +++ b/sources/AppBundle/Event/Model/Repository/TicketRepository.php @@ -356,6 +356,11 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor 'fieldName' => 'transportDistance', 'type' => 'int' ]) + ->addField([ + 'columnName' => 'qr_code', + 'fieldName' => 'qrCode', + 'type' => 'string', + ]) ; return $metadata; diff --git a/sources/AppBundle/Event/Model/SponsorScan.php b/sources/AppBundle/Event/Model/SponsorScan.php new file mode 100644 index 000000000..b9f9228d8 --- /dev/null +++ b/sources/AppBundle/Event/Model/SponsorScan.php @@ -0,0 +1,133 @@ +id; + } + + /** + * @param int $id + * @return $this + */ + public function setId($id) + { + $this->propertyChanged('id', $this->id, $id); + $this->id = $id; + return $this; + } + + /** + * @return string + */ + public function getSponsorTicketId() + { + return $this->sponsorTicketId; + } + + /** + * @param string $sponsorTicketId + * @return $this + */ + public function setSponsorTicketId($sponsorTicketId) + { + $this->propertyChanged('token', $this->sponsorTicketId, $sponsorTicketId); + $this->sponsorTicketId = $sponsorTicketId; + return $this; + } + + /** + * @return \DateTime + */ + public function getCreatedOn() + { + return $this->createdOn; + } + + /** + * @param \DateTime $createdOn + * @return $this + */ + public function setCreatedOn($createdOn) + { + $this->propertyChanged('createdOn', $this->createdOn, $createdOn); + $this->createdOn = $createdOn; + return $this; + } + + /** + * @return \DateTime|null + */ + public function getDeletedOn() + { + return $this->deletedOn; + } + + /** + * @param \DateTime|null $deletedOn + * @return $this + */ + public function setDeletedOn($deletedOn) + { + $this->propertyChanged('deletedOn', $this->deletedOn, $deletedOn); + $this->deletedOn = $deletedOn; + return $this; + } + + /** + * @return int + */ + public function getTicketId() + { + return $this->ticketId; + } + + /** + * @param int $ticketId + * @return $this + */ + public function setTicketId($ticketId) + { + $this->propertyChanged('manager', $this->ticketId, $ticketId); + $this->ticketId = $ticketId; + + return $this; + } +} diff --git a/sources/AppBundle/Event/Model/Ticket.php b/sources/AppBundle/Event/Model/Ticket.php index 1015c9e5b..4b83e4630 100644 --- a/sources/AppBundle/Event/Model/Ticket.php +++ b/sources/AppBundle/Event/Model/Ticket.php @@ -262,6 +262,11 @@ class Ticket implements NotifyPropertyInterface */ protected $transportDistance; + /** + * @var null|string + */ + protected $qrCode; + /** * @return int */ @@ -824,4 +829,24 @@ public function setTransportDistance($transportDistance) return $this; } + + /** + * @return null|string + */ + public function getQrCode() + { + return $this->qrCode; + } + + /** + * @param null|string $qrCode + * @return $this + */ + public function setQrCode($qrCode) + { + $this->propertyChanged('qrCode', $this->qrCode, $qrCode); + $this->qrCode = $qrCode; + + return $this; + } }