Bläddra i källkod

gestion inscriptions et désinscriptions

garthh 22 timmar sedan
förälder
incheckning
f5c13d065c

+ 1 - 0
.env

@@ -44,6 +44,7 @@ MAILER_DSN=null://null
 # Adresse de courriel pour l'envoi des messages
 APP_URL=http://localhost:8000
 APP_TZ="Europe/Paris"
+APP_PHONE_REGION=FR
 CONTACT_EMAIL=no-reply@mail.com
 CONTACT_NAME=Orgasso
 APP_ALLOW_REGISTER=true

+ 7 - 0
assets/controllers/bulma_modal_controller.js

@@ -4,7 +4,13 @@ import { Controller } from "@hotwired/stimulus"
 export default class extends Controller {
     connect() {
         this._closeListener = () => this.close()
+        this._escapeListener = (event) => {
+            if (event.key === 'Escape') {
+                this.close()
+            }
+        }
         window.addEventListener('modal:close', this._closeListener)
+        document.addEventListener('keydown', this._escapeListener)
 
         // Écoute tous les clics sur .open-modal
         document.querySelectorAll('.open-modal').forEach(element => {
@@ -14,6 +20,7 @@ export default class extends Controller {
 
     disconnect() {
         window.removeEventListener('modal:close', this._closeListener)
+        document.removeEventListener('keydown', this._escapeListener)
 
         // Nettoie les événements attachés
         document.querySelectorAll('.open-modal').forEach(element => {

+ 1 - 1
config/packages/misd_phone_number.yaml

@@ -10,4 +10,4 @@ misd_phone_number:
     form: false
     serializer: false
     validator:
-        default_region: FR
+        default_region: '%env(APP_PHONE_REGION)%'

+ 33 - 0
migrations/Version20250804215133.php

@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DoctrineMigrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+
+/**
+ * Auto-generated Migration: Please modify to your needs!
+ */
+final class Version20250804215133 extends AbstractMigration
+{
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function up(Schema $schema): void
+    {
+        // this up() migration is auto-generated, please modify it to your needs
+        $this->addSql('CREATE TABLE participation (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', party_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', participant_name VARCHAR(255) NOT NULL, participant_email VARCHAR(255) NOT NULL, participant_phone VARCHAR(35) DEFAULT NULL COMMENT \'(DC2Type:phone_number)\', consent_mail TINYINT(1) DEFAULT NULL, consent_image TINYINT(1) DEFAULT NULL, INDEX IDX_AB55E24F213C1059 (party_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE participation ADD CONSTRAINT FK_AB55E24F213C1059 FOREIGN KEY (party_id) REFERENCES party (id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE participation DROP FOREIGN KEY FK_AB55E24F213C1059');
+        $this->addSql('DROP TABLE participation');
+    }
+}

+ 17 - 2
src/Controller/MainController.php

@@ -9,6 +9,7 @@ use Symfony\Component\Routing\Attribute\Route;
 
 use App\Repository\EventRepository;
 use App\Entity\Event;
+use App\Entity\Gamemaster;
 
 final class MainController extends AbstractController
 {
@@ -48,7 +49,7 @@ final class MainController extends AbstractController
         // Récupérer la liste des événements visibles
         $events = $repository->findEventsToCome($onlyPublicAccess);
         $event = $events[0];
-        
+
 
         return $this->render('main/booking.html.twig', [
             'event' => $event,
@@ -62,7 +63,7 @@ final class MainController extends AbstractController
         // Contrôler qu'un événement est bien ok
         if (!$event) {
             $this->addFlash('danger', 'Événement inconnu !');
-            $this->redirectToRoute('app_main');
+            $this->redirectToRoute('app_main_booking_main');
         }
 
         // Est-ce qu'un utilisateur est connecté et qu'il est au moins "staff" ?
@@ -127,4 +128,18 @@ final class MainController extends AbstractController
             'codeContent' => $codeContent,
         ]);
     }
+
+    #[Route('/gamemaster/{id}', name: 'app_gamemaster_public_profile', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function gamemasterPublicProfile(?Gamemaster $gamemaster): Response
+    {
+        if ($gamemaster) {
+            return $this->render('main/_modal.gamemaster.html.twig', [
+                'gamemaster' => $gamemaster,
+            ]);
+            
+        } else {
+            $this->addFlash('danger', 'Meneur(euse) de jeu inconnu(e).');
+            return $this->redirectToRoute('app_main');
+        }
+    }
 }

+ 142 - 0
src/Controller/ParticipationController.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace App\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Requirement\Requirement;
+use Symfony\Component\Routing\Attribute\Route;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bridge\Twig\Mime\TemplatedEmail;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mime\Email;
+
+use App\Entity\Slot;
+use App\Entity\Party;
+use App\Entity\Event;
+use App\Entity\Participation;
+use App\Repository\SlotRepository;
+use App\Form\ParticipationType;
+
+final class ParticipationController extends AbstractController
+{
+    #[Route('/cancel/{id}', name: 'app_participation_cancel', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function cancel(?Participation $participation, Request $request, EntityManagerInterface $manager): Response
+    {
+        if (!$participation) {
+            // TGCM, ça ressemble à un UUID, mais y'a rien derrière
+            $this->addFlash('danger', 'Participation inexistante !');
+            return $this->redirectToRoute('app_main');
+        }
+        $form = $this->createFormBuilder(FormType::class)->getForm();
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            $game = $participation->getParty()->getGame()->getName();
+            $gameDate = $participation->getParty()->getStartOn();
+            $gameDate->setTimezone(new \DateTimeZone($_ENV['APP_TZ']));
+            $gameDateStr = $gameDate->format('d/m/y H:i');
+            $manager->remove($participation);
+            $manager->flush();
+            $this->addFlash('info', 'Votre participation à '.$game.' du '.$gameDateStr.' a bien été annulée.');
+            return $this->redirectToRoute('app_main');
+        }
+
+        return $this->render('participation/cancel.html.twig' , [
+            'form' => $form,
+            'participation' => $participation,
+        ]);
+
+    }
+
+    // afficher les détails d'une partie à partir d'un slot et permettre l'inscription
+    #[Route('/party/participation/{id}', name: 'app_participation', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
+    public function partiticipation(?Slot $slot, Request $request, SlotRepository $slotRepository, EntityManagerInterface $manager, MailerInterface $mailer): Response
+    {
+        $party = $slot->getParty();
+        if (!$party) {
+            $this->addFlash('danger', 'Aucune partie associée à ce slot !');
+            //return $this->redirectToRoute('app_main'); // @todo: à modifier !
+            $referer = $request->headers->get('referer'); 
+            return $this->redirect($referer);
+        }
+
+        $user=$this->getUser();
+        // Si un utilisateur est connecté et que ce n'est ni un admin, ni un gestionnaire, ni le MJ de cette partie
+        if ($user) {
+            $roles = $user->getRoles();
+            // Gestionnaire ou admin -> annuler l'user
+            if (in_array("ROLE_MANAGER", $roles) || in_array("ROLE_ADMIN", $roles)) {
+                $user = null;
+            } elseif (in_array("ROLE_STAFF", $roles) && $party->getGamemaster() == $user->getLinkToGamemaster()) {
+                $user = null;
+            }
+        }
+
+        // Préparation du formulaire de participation
+        $participation = new Participation();
+        $participation->setParty($party);
+        if ($user) {
+            $participation->setParticipantName($user->getFullName());
+            $participation->setParticipantEmail($user->getEmail());
+            // @todo: réger le problème des type téléphone dans user et gamemaster !
+            // $participation->setParticipantPhone($user->getPhone()); 
+        }
+        
+        $form = $this->createForm(ParticipationType::class, $participation);
+        
+        // Traitement des entrées du formulaire
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            // On contrôle qu'il y a encore de la place, sinon -> erreur
+            if ($party->getSeatsLeft() < 1) {
+                if ($party->getSeatsLeft() > 0) {
+                    $this->addFlash('danger', 'Il n\'y a pas assez de places pour vous accueillir sur cette partie.');
+                } else {
+                    $this->addFlash('danger', 'Il n\'y a plus de places pour vous accueillir sur cette partie.');
+                }
+            } else {
+                $reservationsCounter = 1;
+
+                // @todo: réservation multiples
+
+                // On enregistre dans la base
+                $manager->persist($participation);
+                $manager->flush();
+                
+                // Envoyer un mail
+                // Maintenant uniquement pour avoir l'ID
+                $email = (new TemplatedEmail())
+                    ->from(new Address($_ENV['CONTACT_EMAIL'], $_ENV['CONTACT_NAME']))
+                    ->to((string) $participation->getParticipantEmail())
+                    ->subject('Votre réservation pour '.$party->getGame()->getName())
+                    ->htmlTemplate('participation/booking.email.html.twig')
+                    ->textTemplate('participation/booking.email.txt.twig')
+                    ->context([
+                        'participation' => $participation,
+                    ]);
+                $mailer->send($email);
+                // On informe
+                if ($reservationsCounter > 1) {
+                    $this->addFlash('success', $reservationsCounter.' réservations enregistrées.');
+                } else {
+                    $this->addFlash('success', 'Réservation enregistrée.');
+                }
+            }
+
+            //$this->redirectToRoute('app_main_booking', ['id' => $party->getEvent()->getId()]);
+            $referer = $request->headers->get('referer'); 
+            return $this->redirect($referer);
+
+        }
+        // Affichage de la modale
+        return $this->render('participation/_modal.add.html.twig', [
+            'party' => $party,
+            'slot' => $slot,
+            'form' => $form,
+            'pathController' => 'app_participation'
+        ]);
+
+    }
+}

+ 4 - 24
src/Controller/PartyController.php

@@ -23,13 +23,13 @@ use App\Security\Voter\SlotAccessVoter;
 
 final class PartyController extends AbstractController
 {
-    #[Route('/party', name: 'app_party')]
+/*     #[Route('/party', name: 'app_party')]
     public function index(): Response
     {
         return $this->render('party/index.html.twig', [
             'controller_name' => 'PartyController',
         ]);
-    }
+    } */
 
     // supprimer une partie
     #[Route('/party/delete/{id}', name: 'app_party_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
@@ -160,7 +160,7 @@ final class PartyController extends AbstractController
             return $this->redirect($referer);
         }
 
-        return $this->render('party/edit.html.twig', [
+        return $this->render('party/_modal.edit.html.twig', [
             'form' => $form,
             'party' => $party,
             'gamemasters' => $gamemasters,
@@ -172,26 +172,6 @@ final class PartyController extends AbstractController
         ]);
     }
 
-    // afficher les détails d'une partie à partir d'un slot
-    #[Route('/party/view/{id}', name: 'app_party_view', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
-    public function view(?Slot $slot, Request $request, SlotRepository $slotRepository, EntityManagerInterface $manager): Response
-    {
-        $party = $slot->getParty();
-        if (!$party) {
-            $this->addFlash('danger', 'Aucune partie associée à ce slot !');
-            //return $this->redirectToRoute('app_main'); // @todo: à modifier !
-            $referer = $request->headers->get('referer'); 
-            return $this->redirect($referer);
-        }
-
-        return $this->render('party/view.html.twig', [
-            'party' => $party,
-            'slot' => $slot,
-        ]);
-
-    }
-
-
     // ajouter une partie
     #[Route('/party/add/{id}', name: 'app_party_add', requirements: ['id' => '\d+'], methods: ['GET','POST'])]
     public function add(?Slot $slot, Request $request, SlotRepository $slotRepository, GamemasterRepository $gamemasterRepository, GameRepository $gameRepository, EntityManagerInterface $manager): Response
@@ -265,7 +245,7 @@ final class PartyController extends AbstractController
 
         }
 
-        return $this->render('party/edit.html.twig', [
+        return $this->render('party/_modal.edit.html.twig', [
             'form' => $form,
             'party' => $party,
             'gamemasters' => $gamemasters,

+ 123 - 0
src/Entity/Participation.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\ParticipationRepository;
+use Doctrine\ORM\Mapping as ORM;
+use libphonenumber\PhoneNumber;
+use Symfony\Bridge\Doctrine\Types\UuidType;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\Uid\Uuid;
+
+#[ORM\Entity(repositoryClass: ParticipationRepository::class)]
+class Participation
+{
+    #[ORM\Id]
+    #[ORM\Column(type: UuidType::NAME, nullable: true)]
+    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
+    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
+    private ?Uuid $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'participations')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Party $party = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $participantName = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $participantEmail = null;
+
+    #[ORM\Column(type: 'phone_number', nullable: true)]
+    private ?PhoneNumber $participantPhone = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $consentMail = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $consentImage = null;
+
+    public function getId(): ?Uuid
+    {
+        return $this->id;
+    }
+
+    public function setId(Uuid $id): static
+    {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    public function getParty(): ?Party
+    {
+        return $this->party;
+    }
+
+    public function setParty(?Party $party): static
+    {
+        $this->party = $party;
+
+        return $this;
+    }
+
+    public function getParticipantName(): ?string
+    {
+        return $this->participantName;
+    }
+
+    public function setParticipantName(string $participantName): static
+    {
+        $this->participantName = $participantName;
+
+        return $this;
+    }
+
+    public function getParticipantEmail(): ?string
+    {
+        return $this->participantEmail;
+    }
+
+    public function setParticipantEmail(string $participantEmail): static
+    {
+        $this->participantEmail = $participantEmail;
+
+        return $this;
+    }
+
+    public function getParticipantPhone(): ?PhoneNumber
+    {
+        return $this->participantPhone;
+    }
+
+    public function setParticipantPhone(?PhoneNumber $participantPhone): static
+    {
+        $this->participantPhone = $participantPhone;
+
+        return $this;
+    }
+
+    public function isConsentMail(): ?bool
+    {
+        return $this->consentMail;
+    }
+
+    public function setConsentMail(?bool $consentMail): static
+    {
+        $this->consentMail = $consentMail;
+
+        return $this;
+    }
+
+    public function isConsentImage(): ?bool
+    {
+        return $this->consentImage;
+    }
+
+    public function setConsentImage(?bool $consentImage): static
+    {
+        $this->consentImage = $consentImage;
+
+        return $this;
+    }
+}

+ 49 - 0
src/Entity/Party.php

@@ -64,9 +64,16 @@ class Party
     #[ORM\Column(nullable: true)]
     private ?bool $validated = null;
 
+    /**
+     * @var Collection<int, Participation>
+     */
+    #[ORM\OneToMany(targetEntity: Participation::class, mappedBy: 'party', orphanRemoval: true)]
+    private Collection $participations;
+
     public function __construct()
     {
         $this->slots = new ArrayCollection();
+        $this->participations = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -215,6 +222,18 @@ class Party
         return $this;
     }
 
+    public function getSeatsOccuped(): ?int 
+    {
+        $seatsOccuped = count($this->getParticipations());
+        return $seatsOccuped;
+    }
+
+    public function getSeatsLeft(): ?int
+    {
+        $seatsLeft = $this->maxParticipants - count($this->getParticipations());
+        return $seatsLeft;
+    }
+
     public function getDescription(): ?string
     {
         return $this->description;
@@ -262,4 +281,34 @@ class Party
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Participation>
+     */
+    public function getParticipations(): Collection
+    {
+        return $this->participations;
+    }
+
+    public function addParticipation(Participation $participation): static
+    {
+        if (!$this->participations->contains($participation)) {
+            $this->participations->add($participation);
+            $participation->setParty($this);
+        }
+
+        return $this;
+    }
+
+    public function removeParticipation(Participation $participation): static
+    {
+        if ($this->participations->removeElement($participation)) {
+            // set the owning side to null (unless already changed)
+            if ($participation->getParty() === $this) {
+                $participation->setParty(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 67 - 0
src/Form/ParticipationType.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Participation;
+use App\Entity\Party;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ParticipationType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('participantName', null, [
+                'label' => 'Prénom et nom, ou pseudonyme',
+                'required' => true,
+                'help' => 'Une identité pour vérifier votre présence à la table.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('participantEmail', null, [
+                'label' => 'email',
+                'required' => true,
+                'help' => 'Entrez une adresse email. Utilisée pour confirmer votre inscription.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('participantPhone', null, [
+                'label' => 'Numéro de téléphone mobile',
+                'required' => false,
+                'help' => 'Entrez un numéro de téléphone mobile (optionnel, pour vous contacter en urgence au sujet de votre inscription).',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('consentMail', null, [
+                'label' => 'Je souhaites recevoir par courriel les informations de l\'association.',
+                'label_attr' => ['class' => 'checkbox'],
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+            ])
+            ->add('consentImage', null, [
+                'label' => 'J\'autorise la diffusion de mon image',
+                'help' => 'En cas de prise de photo, j\'autorise l\'association aux Portes de l\'Imaginaire à diffuser mon image sur tout support de communication (site Internet, réseau social...) pour communiquer sur ses activités. En cas de refus, mon visage sera flouté sur toute diffusion de l\'image.',
+                'label_attr' => ['class' => 'checkbox'],
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Participation::class,
+        ]);
+    }
+}

+ 43 - 0
src/Repository/ParticipationRepository.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Participation;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Participation>
+ */
+class ParticipationRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Participation::class);
+    }
+
+    //    /**
+    //     * @return Participation[] Returns an array of Participation objects
+    //     */
+    //    public function findByExampleField($value): array
+    //    {
+    //        return $this->createQueryBuilder('p')
+    //            ->andWhere('p.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->orderBy('p.id', 'ASC')
+    //            ->setMaxResults(10)
+    //            ->getQuery()
+    //            ->getResult()
+    //        ;
+    //    }
+
+    //    public function findOneBySomeField($value): ?Participation
+    //    {
+    //        return $this->createQueryBuilder('p')
+    //            ->andWhere('p.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->getQuery()
+    //            ->getOneOrNullResult()
+    //        ;
+    //    }
+}

+ 1 - 1
templates/components/Planning.html.twig

@@ -75,7 +75,7 @@
 
                                                 </div>
                                                 <div class="card-content">
-                                                    <small>{{ thisSlot.party.game.description }}</small>
+                                                    <small>Places : {{ thisSlot.party.getSeatsLeft }} / {{ thisSlot.party.getMaxParticipants }}</small>
                                                 </div>
 
                                             

+ 31 - 0
templates/main/_modal.gamemaster.html.twig

@@ -0,0 +1,31 @@
+{% extends 'modal.html.twig' %}
+
+{% block title %}{{ gamemaster.preferedName }}{% endblock %}
+
+{% block content %}
+<article class="media">
+  <figure class="media-left">
+    <p class="image is-128x128">
+        {% if gamemaster.picture %}
+        <img class="is-rounded" src="/images/gamemasters/{{ gamemaster.picture }}" />
+        {% else %}
+        <twig:ux:icon name="bi:person-fill"/>
+        {% endif %}
+    </p>
+  </figure>
+  <div class="media-content">
+    <div class="content">
+      <p>
+       {{ gamemaster.description }}
+      </p>
+      <hr/>
+      <p>
+        <em>Je peux animer des parties des jeux suivants : </em>
+       {{ gamemaster.gamesCanMaster|map(game => game.name)|join(', ') }}
+
+      </p>
+    </div>
+  </div>
+</article>
+{% endblock %}
+

+ 4 - 9
templates/main/booking.html.twig

@@ -50,6 +50,7 @@
       <p class="subtitle is-4">du {{ event.startOn|date('d/m/y H:i', app_timezone) }} au {{ event.endOn|date('d/m/y H:i', app_timezone)}}</p>
       <article>
         <p>{{ event.description }}</p>
+        {% if event.moreLink %}<p>En savoir plus : <a href="{{ event.moreLink }}" target="_blank">{{ event.moreLink }}</a></p>{% endif %}
       </article>
     </div>
   </div>
@@ -68,9 +69,9 @@
       <p class="is-inline-block icon-text">{{ event.getGamemastersAssigned|length }} meneur(euse)s de jeu
         {% for gamemaster in event.getGamemastersAssigned %}
           <span class="icon">
-              <a href="#" class="open-modal"><figure class="image is-24x24 is-inline-block">
+              <a href="{{ path('app_gamemaster_public_profile', {id: gamemaster.id}) }}" class="open-modal"><figure class="image is-24x24 is-inline-block">
               {% if gamemaster.picture %}
-              <img class="is-rounded" src="/images/gamemasters/{{ gamemaster.picture }}"  />
+              <img class="is-rounded" src="/images/gamemasters/{{ gamemaster.picture }}" />
               {% else %}
               <twig:ux:icon name="bi:person-fill"/>
               {% endif %}
@@ -91,14 +92,8 @@
 {% if not event.isHiddenPlanning %}
 <section>
   <hr />
-      {% if is_granted('ROLE_STAFF') %}
-      <div class="block">
-      <p class="icon-text">MJ, cliquez sur un <span class="icon"><twig:ux:icon name="bi:plus-circle"/></span> pour ajouter une partie</p>
-      </div>
-    {% endif %}
-
   <div id="planning">
-    {{ component('Planning', {event: event,pathFullSlot: 'app_party_view'}) }}
+    {{ component('Planning', {event: event,pathFullSlot: 'app_participation'}) }}
   </div>
 </section>
 {% endif %}

+ 2 - 3
templates/modal.html.twig

@@ -2,13 +2,12 @@
     <div class="modal-card-head">
         <h1 class="title is-3">{% block title %}{% endblock %}</h1>
     </div>
+    {% block header %}
+    {% endblock %}
     <section class="modal-card-body">
         {% block content %}
         {% endblock %}
     </section>
     {% block footer %}
-    <section class="modal-card-footer">
-        
-    </section>
     {% endblock %}
 </div>

+ 120 - 0
templates/participation/_modal.add.html.twig

@@ -0,0 +1,120 @@
+{% extends 'modal.html.twig' %}
+
+{% block title %}{{ party.game.name }}{% endblock %}
+
+{% block header %}
+{% if party.getGame.getPicture %}
+  <section class="modal-card-image">
+    <figure class="image is-3by1">
+      <img
+        src="/images/games/{{ party.getGame.getPicture }}"
+        alt="{{ party.getGame.getName }}"
+      />
+    </figure>
+  </section>
+{% endif %}
+{% endblock %}
+
+{% block content %}
+  <div class="content">
+    <div class="columns">
+      <div class="column has-text-right is-one-quarter">
+        <strong>Horaires</strong>
+      </div>
+    <div class="column has-text-left">
+        {{ party.getStartOn|date('d/m/y H:i', app_timezone)}} à {{ party.getEndOn|date('d/m/y H:i', app_timezone)}}
+      </div>
+    </div>
+    <div class="columns">
+      <div class="column has-text-right is-one-quarter">
+        <strong>Espace</strong>
+      </div>
+      <div class="column has-text-left">
+        {{ party.getSlots.first.getSpace.getName }}
+      </div>
+    </div>
+    <div class="columns">
+      <div class="column has-text-right is-one-quarter">
+        <strong>Meneur(euse)</strong>
+      </div>
+      <div class="column has-text-left">
+        <span class="icon">
+            <figure class="image is-24x24 is-inline-block">
+            {% if party.getGamemaster.picture %}
+            <img class="is-rounded" src="/images/gamemasters/{{ party.getGamemaster.picture }}" />
+            {% else %}
+            <twig:ux:icon name="bi:person-fill"/>
+            {% endif %}
+            </figure>
+        </span> 
+        {{ party.getGamemaster.getPreferedName }}
+      </div>
+    </div>
+    <div class="columns">
+      <div class="column has-text-right is-one-quarter">
+        <strong>Jeu</strong>
+      </div>
+      <div class="column has-text-left">
+        {{ party.getGame.getName }}
+      </div>
+    </div>
+  </div>
+
+ <div class="tabs is-boxed" {{ stimulus_controller('bulma_tabs') }}>
+   <ul>
+     <li class="is-active" data-id="tab-1"><a>Le jeu</a></li>
+     {% if party.getDescription %}<li data-id="tab-2"><a>Le scénario</a></li>{% endif %}
+     {% if party.getSeatsLeft > 0 %}<li data-id="tab-3"><a>S'incrire</a></li>{% endif %}
+   </ul>
+ </div>
+ 
+ <div id="tabs-content">
+    <div class="container" id="tab-1">
+      <p>{{ party.getGame.getDescription }}</p>
+    </div>
+    {% if party.getDescription %}
+    <div class="container is-hidden" id="tab-2">
+      <p>{{ party.getDescription }}</p>
+      
+    </div>
+    {% endif %}
+    {% if party.getSeatsLeft > 0 %}
+    <div class="container is-hidden" id="tab-3">
+    
+    {{ form_errors(form) }}
+    {{ form_start(form, {action: path(pathController, {id: party.slots.first.getId}), attr: {'data-turbo-frame': 'modal-content'}}) }}
+
+    {{ form_row(form.participantName) }}
+    {{ form_row(form.participantEmail) }}
+    {{ form_row(form.participantPhone) }}
+
+    <div class="field">
+      {{ form_widget(form.consentMail) }}
+      {{ form_label(form.consentMail) }}
+    </div>
+
+    <div class="field">
+      {{ form_widget(form.consentImage) }}
+      {{ form_label(form.consentImage) }}
+      {{ form_help(form.consentImage) }}
+    </div>
+
+    {{ form_widget(form)}}
+
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+    </div>
+    
+    {{ form_end(form) }}
+
+    </div>
+    {% endif %}
+ </div>
+
+
+{% endblock %}
+
+
+
+
+

+ 52 - 0
templates/participation/booking.email.html.twig

@@ -0,0 +1,52 @@
+{% extends 'base.email.html.twig' %}
+
+{% block title %}Participation enregistrée{% endblock %}
+{% block content %}
+                    <tr>
+                        <td style="padding: 0 20px 10px 20px; font-size: 16px;">
+                            <p style="margin: 0;">Bonjour {{ participation.participantName }},</p>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px; font-size: 16px;">
+                            <p style="margin: 0;">Une place est réservée pour la partie de jeu de rôle suivante :</p>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px;">
+                            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-size: 15px; line-height: 1.6;">
+                                <tr>
+                                    <td><strong>Événement :</strong></td>
+                                    <td>{{ participation.party.event.name }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Jeu :</strong></td>
+                                    <td>{{ participation.party.getGame.name }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Date :</strong></td>
+                                    <td>{{ participation.party.getStartOn|date('d/m/y', app_timezone) }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Heure :</strong></td>
+                                    <td>{{ participation.party.getStartOn|date('H:i', app_timezone) }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Espace :</strong></td>
+                                    <td>{{ participation.party.slots.first.space.name }}</td>
+                                </tr>
+                            </table>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px; font-size: 15px;">
+                            <p style="margin: 0;">Préparez vos dés et votre imagination ! L'aventure vous attend...</p>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 20px; font-size: 12px; text-align: center; color: #777;">
+                            En cas de désistement, merci d'annuler au plus vite en cliquant sur le lien suivant<br/>
+                            <a href="{{ app_url }}/cancel/{{ participation.getId|trans }}">{{ app_url }}/cancel/{{ participation.getId|trans }}</a>
+                        </td>
+                    </tr>
+{% endblock %}

+ 14 - 0
templates/participation/booking.email.txt.twig

@@ -0,0 +1,14 @@
+Bonjour {{ participation.participantName }},
+
+Une place est réservée pour la partie de jeu de rôle suivante :
+
+Événement : {{ participation.party.event.name }}
+Jeu : {{ participation.party.getGame.name }}
+Date : {{ participation.party.getStartOn|date('d/m/y', app_timezone) }}
+Heure : {{ participation.party.getStartOn|date('H:i', app_timezone) }}
+Espace : {{ participation.party.getSlots.first.getSpace.getName }}
+
+Préparez vos dés et votre imagination ! L'aventure vous attend...
+
+En cas de désistement, merci d'annuler au plus vite en cliquant sur le lien suivant
+{{ app_url }}/cancel/{{ participation.getId|trans }}

+ 28 - 0
templates/participation/cancel.html.twig

@@ -0,0 +1,28 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Confirmez votre annulation{% endblock %}
+
+{% block content %}
+
+<div class="container">
+  <div class="columns is-centered">
+    <div class="column is-5">
+      <div class="box">
+        <h1 class="title is-4 has-text-centered">Annuler</h1>
+        
+        <p>Merci de cliquer sur <span class="has-text-danger">Confirmer</span> pour annuler votre participation à la partie de {{ participation.party.game.name }} du {{ participation.party.startOn|date('d/m/y H:i', app_timezone) }}.</p>
+
+        <div class="field is-grouped is-grouped-centered">
+          <div class="control">
+            {{ form_start(form) }}
+            {{ form_widget(form) }}
+            <button class="button is-danger" type="submit">Confirmer</button>
+            <a href="{{ path('app_main') }}" class="button">Retour</a>
+            {{ form_end(form) }}
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock %}

+ 0 - 0
templates/party/edit.html.twig → templates/party/_modal.edit.html.twig


+ 0 - 27
templates/party/view.html.twig

@@ -1,27 +0,0 @@
-{% extends 'modal.html.twig' %}
-
-{% block title %}{{ party.game.name }}{% endblock %}
-
-{% block content %}
-
-    <section class="hero is-info">
-    <div class="hero-body">
-        <p class="title">{{ party.game.name }}</p>
-        <p class="subtitle">Date : {{ party.startOn|date('d/m/y', app_timezone) }} de {{ party.startOn|date('H:i', app_timezone) }} à {{ party.endOn|date('H:i', app_timezone) }} • Espace : {{ party.slots[0].space.name }} • Meneur(euse) de jeu : {{ party.gamemaster.preferedName }}</p>
-    </div>
-    </section>
-    <section class="content">
-        <p>{{ party.game.description }}</p>
-    </section>
-    
-    
-
-
-{% endblock %}
-
- {% block footer %}
-
-    <footer class="modal-card-footer">
-        M'inscrire
-    </footer>
-    {% endblock %}