Browse Source

gestion des demandes

garthh 3 weeks ago
parent
commit
77aa821f3d

+ 93 - 0
assets/controllers/request_process_controller.js

@@ -0,0 +1,93 @@
+
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+    connect() {
+        // Écoute tous les clics sur .open-modal -> détournement vers #lastStep + MAJ des options du formulaire
+        document.querySelectorAll('.planning-cell-free').forEach(element => {
+            element.addEventListener('click', this.handleClick.bind(this))
+        })
+    }
+
+    handleClick(event) {
+        event.preventDefault()
+        const slotId = event.currentTarget.dataset.id
+
+        if (!slotId) return
+
+        // A partir du slotId ->
+        // 1. trouver les slots disponible suviants
+        // 2. mettre à jour le formulaire
+        // 3. forcer un scroll jusqu'à #lastStep
+
+        // Etape 1
+        fetch(`/admin/api/slot/${slotId}/nexts`, {
+          method: 'POST',
+          headers: {
+            'Accept': 'application/json'
+          }
+        })
+          .then(response => {
+            if (!response.ok) {
+              throw new Error(`Erreur API: ${response.status}`);
+            }
+            return response.json();
+          })
+          .then(data => {
+            
+            // Etape 2
+            console.log('ID du slot cliqué :', slotId);
+            console.log('IDs des slots suivants :', data.next_slots);
+            console.log('Horaire de début :', data.start_date);
+            console.log('Horaires de fin :', data.end_dates);
+
+            const select = document.querySelector('#party_slots');
+            select.innerHTML = ''; // Vider les anciennes options
+
+            const ids = data.next_slots;      // ex: [1, 2, 3, 4]
+            const labels = data.end_dates;    // ex: ["07/08/25 14:00", "07/08/25 16:00", "07/08/25 18:00", "07/08/25 20:00"]
+
+            let cumulativeIds = [];
+
+            ids.forEach((id, index) => {
+              cumulativeIds.push(id); // on ajoute l'ID courant à la liste
+
+              const value = cumulativeIds.join('|'); // ex: "1|2|3"
+              const label = labels[index] || `Créneau ${id}`; // on sécurise
+
+              const option = document.createElement('option');
+              option.value = value;
+              option.textContent = label;
+
+              select.appendChild(option);
+            });
+
+            const start = document.querySelector('#party_start');
+            start.innerHTML = '';
+            start.innerHTML = data.start_date;
+
+            document.querySelector('#lastStep')?.scrollIntoView({ behavior: 'smooth' });
+
+          })
+          .catch(error => {
+            console.error('Erreur lors de la récupération des slots :', error);
+          });
+
+
+
+        
+
+
+
+
+
+    }
+
+    disconnect() {
+    this.element.querySelectorAll('.planning-cell-free').forEach(element => {
+        element.removeEventListener('click', this.handleClick)
+    })
+}
+
+
+}

+ 4 - 0
assets/styles/app.css

@@ -39,6 +39,10 @@
   transform: translateY(0);
 }
 
+.jump-page {
+  margin-bottom: 100vh;
+}
+
 /* Styles pour les plannings */
 
 /* Style par défaut pour les cellules

+ 4 - 2
config/packages/security.yaml

@@ -40,9 +40,9 @@ security:
     role_hierarchy:
         # USER : utilisateur simple authentifié, suivi des réservations de ses parties, annulations, demandes de parties...
         ROLE_USER: ~
-        # STAFF : utilisateur avec des droits étendus, gestion des parties, des utilisateurs, des gamemasters...
+        # STAFF : utilisateur membre du staff, ex. MJ...
         ROLE_STAFF: [ROLE_USER]
-        # MANAGER : utilisateur avec des droits étendus, gestion des parties, des utilisateurs, des gamemasters...
+        # MANAGER : utilisateur avec des droits étendus, gestion des parties et suivi des inscriptions...
         ROLE_MANAGER: [ROLE_STAFF]
         # ADMIN : utilisateur avec des droits étendus, gestion des parties, des utilisateurs, des gamemasters...
         ROLE_ADMIN: [ROLE_MANAGER]
@@ -53,6 +53,8 @@ security:
         - { path: ^/admin, roles: ROLE_ADMIN }
         - { path: ^/profile, roles: ROLE_USER }
         - { path: ^/manage, roles: ROLE_MANAGER }
+        - { path: ^/prepare, roles: ROLE_STAFF }
+        - { path: ^/check, roles: ROLE_STAFF }
 
 when@test:
     security:

+ 32 - 0
src/Controller/Admin/EventConfig/SlotController.php

@@ -5,6 +5,7 @@ namespace App\Controller\Admin\EventConfig;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\Routing\Requirement\Requirement;
 use Symfony\Component\Routing\Attribute\Route;
 use Doctrine\ORM\EntityManagerInterface;
@@ -17,6 +18,37 @@ use App\Service\DateTimeHelper;
 
 final class SlotController extends AbstractController
 {
+    #[Route('/admin/api/slot/{id}/nexts', name: 'app_admin_api_slot_next', requirements: ['id' => '\d+'], methods: ['POST'])]
+    public function adminApiNexts(?Slot $slot, SlotRepository $repository): JsonResponse
+    {
+        if (!$slot) {
+            return $this->json(['error' => 'Slot not found'], 404);
+        }
+
+        $nextSlots = $repository->findNextsAvailables($slot);
+        array_unshift($nextSlots, $slot);
+
+        // On extrait les IDs des slots suivants
+        $ids = array_map(fn($s) => $s->getId(), $nextSlots);
+        
+        // On choppe aussi début et un tableau des horaires de fin
+        $startDate = $slot->getStartOn();
+        $startDate->setTimezone(new \DateTimeZone($_ENV['APP_TZ']));
+        $startDateStr = $startDate->format('d/m/y H:i');
+
+        $endDatesStr = array_map(fn($s) => $s->getEndOn()
+                                          ->setTimezone(new \DateTimeZone($_ENV['APP_TZ']))
+                                          ->format('d/m/y H:i'),
+                              $nextSlots);
+
+        
+        return $this->json(["next_slots" => $ids,
+                            "start_date" => $startDateStr,
+                            "end_dates" => $endDatesStr]);
+    }
+
+
+
     #[Route('/admin/event/{id}/configure/slot', name: 'app_admin_event_config_slot', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
     public function index(?Event $event, Request $request, EntityManagerInterface $manager, SlotRepository $repository): Response
     {

+ 1 - 1
src/Controller/MainController.php

@@ -124,7 +124,7 @@ final class MainController extends AbstractController
     #[Route('/contact', name: 'app_contact')]
     public function contact(): Response
     {
-        // @todo: formulaire de contact avec envoi d'un mail
+        // TODO: formulaire de contact avec envoi d'un mail
         // Lire le contenu de la charte des animations depuis un fichier
         $codeContent = file_get_contents(__DIR__ . '/../../public/pages/legal.md');
 

+ 214 - 3
src/Controller/ManageController.php

@@ -3,11 +3,21 @@
 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\partyRequest;
+use App\Entity\Party;
+use App\Entity\Participation;
 use App\Repository\EventRepository;
+use App\Repository\SlotRepository;
 use App\Entity\Event;
 use App\Entity\Gamemaster;
 
@@ -37,7 +47,7 @@ final class ManageController extends AbstractController
         // Contrôler qu'un événement est bien ok
         if (!$event) {
             $this->addFlash('danger', 'Événement inconnu !');
-            $this->redirectToRoute('app_manage');
+            return $this->redirectToRoute('app_manage');
         }
 
         // Récupérer la liste des événements visibles
@@ -55,7 +65,7 @@ final class ManageController extends AbstractController
         // Contrôler qu'un événement est bien ok
         if (!$event) {
             $this->addFlash('danger', 'Événement inconnu !');
-            $this->redirectToRoute('app_manage');
+            return $this->redirectToRoute('app_manage');
         }
 
         // Récupérer la liste des événements visibles
@@ -73,7 +83,7 @@ final class ManageController extends AbstractController
         // Contrôler qu'un événement est bien ok
         if (!$event) {
             $this->addFlash('danger', 'Événement inconnu !');
-            $this->redirectToRoute('app_manage');
+            return $this->redirectToRoute('app_manage');
         }
 
         // Récupérer la liste des événements visibles
@@ -84,4 +94,205 @@ final class ManageController extends AbstractController
             'events' => $events
         ]);
     }
+
+    #[Route('/manage/{id}/request', name: 'app_manage_request', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function manageRequest(?Event $event, EventRepository $repository): Response
+    {
+        // Contrôler qu'un événement est bien ok
+        if (!$event) {
+            $this->addFlash('danger', 'Événement inconnu !');
+            return $this->redirectToRoute('app_manage');
+        }
+
+        // Contrôler que l'événement accepte bien les requêtes
+        if (!$event->isEveryoneCanAskForGame()) {
+            $this->addFlash('danger', 'Cet événement ne prend pas en charge les demandes de parties.');
+            return $this->redirectToRoute('app_manage_planning', ['id' => $event->getId()]);            
+        }
+
+        // Récupérer la liste des événements visibles
+        $events = $repository->findEventsToCome(false);
+
+        return $this->render('manage/request.html.twig', [
+            'event' => $event,
+            'events' => $events
+        ]);
+    }
+
+    #[Route('/manage/request/{id}/delete', name: 'app_manage_request_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function requestDelete(?partyRequest $partyRequest, EntityManagerInterface $manager): Response
+    {
+        // la requête existe-t-elle bien ?
+        if (!$partyRequest) {
+            $this->addFlash('danger', 'Demande inconnue !');
+            return $this->redirectToRoute('app_manage');
+        }
+
+        $event = $partyRequest->getEvent();
+        $state = $partyRequest->getState();
+        // La requête a-t-elle obtenue une réponse ?
+        if ($state['code'] == "WAIT") {
+            $this->addFlash('danger', 'Vous devez apporter une réponse avant de supprimer !');
+            return $this->redirectToRoute('app_manage_request', ['id' => $event->getId()]);
+        }
+
+        $manager->remove($partyRequest);
+        $manager->flush();
+
+        $this->addFlash('success', 'Demande supprimée !');
+        return $this->redirectToRoute('app_manage_request', ['id' => $event->getId()]);
+    }
+
+    #[Route('/manage/request/{id}/accept', name: 'app_manage_request_accept', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function requestAccept(?partyRequest $partyRequest, Request $request, EntityManagerInterface $manager, MailerInterface $mailer, EventRepository $repository, SlotRepository $slotRepository): Response
+    {
+        // Fonction inspirée de 'app_party_add' du controlleur "App\Controller\PartyController"
+        // TODO: créer une seule fonction
+        // PRIO: low
+
+        $user = $this->getUser();
+
+        // la requête existe-t-elle bien ?
+        if (!$partyRequest) {
+            $this->addFlash('danger', 'Demande inconnue !');
+            return $this->redirectToRoute('app_manage');
+        }
+
+        $event = $partyRequest->getEvent();
+        $state = $partyRequest->getState();
+        // La requête est-elle en attente d'une réponse ?
+        if (!$state['code'] == "WAIT") {
+            $this->addFlash('danger', 'Une réponse a déjà été apportée : '.$state['name'].'.');
+            return $this->redirectToRoute('app_manage_planning', ['id' => $event->getId()]);
+        }
+
+        // Traiter, on complète déjà ce qu'on connaît
+        $party = new Party();
+        $party->setGamemaster($partyRequest->getGamemasterChoosen());
+        $party->setGame($partyRequest->getGameChoosen());
+        $party->setEvent($partyRequest->getEvent());
+        $party->setMinParticipants($_ENV['APP_DEFAULT_MIN_PARTICIPANTS']);
+        $party->setMaxParticipants($_ENV['APP_DEFAULT_MAX_PARTICIPANTS']);
+        $party->setSubmitter($user);
+        $party->setSubmittedDate(new \Datetime('now'));
+        $party->setValidated(true);
+
+        // Création d'un formulaire vierge
+        $form = $this->createFormBuilder(FormType::class)->getForm();
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            // On reçoit du formulaire un array de Slot pour poser la partie
+            $slotsString = $request->request->get('party_slots');
+            $new_slots = explode("|", $slotsString);
+            // On ajoute les slots à la partie
+            foreach($new_slots as $add_this_slot) {
+                $add_this_slot_obj = $slotRepository->findById($add_this_slot);
+                $party->addSlot($add_this_slot_obj);
+            }
+            // On ajoute les horaires à la partie
+            $party->setStartOn(clone $party->getSlots()[0]->getStartOn());
+            $party->setEndOn(clone $party->getSlots()->last()->getEndOn());
+
+            // On enregistre la partie
+            $manager->persist($party);
+
+            // On met à jour la demande
+            $partyRequest->setModerator($user);
+            $partyRequest->setModOnDate(new \Datetime('now'));
+            $partyRequest->setAccepted(true);
+            $manager->persist($partyRequest);
+
+            // On crée une participation pour le demandeur
+            $participation = new Participation();
+            $participation->setParty($party);
+            $participation->setParticipantName($partyRequest->getRequester()->getFullName());
+            $participation->setParticipantEmail($partyRequest->getRequester()->getEmail());
+            $participation->setParticipantPhone($partyRequest->getRequester()->getPhone());
+            $manager->persist($participation);
+
+            // Et on sync dans la BDD
+            $manager->flush();
+
+            // On préviens par mail
+            $email = (new TemplatedEmail())
+                ->from(new Address($_ENV['CONTACT_EMAIL'], $_ENV['CONTACT_NAME']))
+                ->to((string) $partyRequest->getRequester()->getEmail())
+                ->subject('En réponse à votre demande pour '.$party->getEvent()->getName())
+                ->htmlTemplate('manage/request/accepted.email.html.twig')
+                ->textTemplate('manage/request/accepted.email.txt.twig')
+                ->context([
+                    'participation' => $participation,
+                    'partyRequest' => $partyRequest,
+                    'party' => $party
+                ]);
+            $mailer->send($email);
+
+            // On a terminé !
+            $this->addFlash('success', 'Demande transformée en partie et réponse envoyée.');
+            return $this->redirectToRoute('app_manage_planning', ['id' => $event->getId()]);
+
+        }
+
+        // génération du formulaire
+
+        // Récupérer la liste des événements visibles
+        $events = $repository->findEventsToCome(false);
+        if (!$events) {
+            $this->addFlash('info', 'Aucun d\'événement n\'est plannifié pour le moment.');
+            return $this->redirectToRoute('app_main');
+        }
+
+        return $this->render('manage/request/process.html.twig', [
+            'partyRequest' => $partyRequest,
+            'party' => $party,
+            'event' => $partyRequest->getEvent(),
+            'events' => $events,
+            'form' => $form
+        ]);
+    }
+
+    #[Route('/manage/request/{id}/refuse', name: 'app_manage_request_refuse', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function requestRefuse(?partyRequest $partyRequest, EntityManagerInterface $manager, MailerInterface $mailer): Response
+    {
+        $user = $this->getUser();
+        // la requête existe-t-elle bien ?
+        if (!$partyRequest) {
+            $this->addFlash('danger', 'Demande inconnue !');
+            return $this->redirectToRoute('app_manage');
+        }
+
+        $event = $partyRequest->getEvent();
+        $state = $partyRequest->getState();
+        // La requête est-elle en attente d'une réponse ?
+        if (!$state['code'] == "WAIT") {
+            $this->addFlash('danger', 'Une réponse a déjà été apportée : '.$state['name'].'.');
+            return $this->redirectToRoute('app_manage_planning', ['id' => $event->getId()]);
+        }
+
+        // On met à jour la demande
+        $partyRequest->setModerator($user);
+        $partyRequest->setModOnDate(new \Datetime('now'));
+        $partyRequest->setAccepted(false);
+        $manager->persist($partyRequest);
+
+        // Et on sync dans la BDD
+        $manager->flush();
+
+        // On préviens par mail
+        $email = (new TemplatedEmail())
+            ->from(new Address($_ENV['CONTACT_EMAIL'], $_ENV['CONTACT_NAME']))
+            ->to((string) $partyRequest->getRequester()->getEmail())
+            ->subject('En réponse à votre demande pour '.$partyRequest->getEvent()->getName())
+            ->htmlTemplate('manage/request/refused.email.html.twig')
+            ->textTemplate('manage/request/refused.email.txt.twig')
+            ->context([
+                'partyRequest' => $partyRequest,
+            ]);
+        $mailer->send($email);
+
+        $this->addFlash('success', 'Demande refusée et réponse envoyée.');
+        return $this->redirectToRoute('app_manage_planning', ['id' => $event->getId()]);
+
+
+    }
 }

+ 1 - 2
src/Controller/ParticipationController.php

@@ -38,7 +38,7 @@ final class ParticipationController extends AbstractController
             $redirectPath = 'app_profile_participations';
         }
 
-        // @todo: prendre en charge l'annulation en une fois de réservations de groupe
+        // TODO: prendre en charge l'annulation en une fois de réservations de groupe
 
         $form = $this->createFormBuilder(FormType::class)->getForm();
         $form->handleRequest($request);
@@ -68,7 +68,6 @@ final class ParticipationController extends AbstractController
         $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);
         }

+ 2 - 8
src/Controller/PartyController.php

@@ -43,7 +43,6 @@ final class PartyController extends AbstractController
         } else {
             $this->addFlash('danger', 'Seuls les admins peuvent supprimer une partie.');
         }
-        //return $this->redirectToRoute('app_main'); // @todo: à modifier !
         $referer = $request->headers->get('referer'); 
         return $this->redirect($referer);  
     }
@@ -67,7 +66,6 @@ final class PartyController extends AbstractController
         } else {
             $this->addFlash('danger', 'Seuls les rôles Gestionnaires et Admin peuvent valider une partie');
         }
-            //return $this->redirectToRoute('app_main'); // @todo: à modifier !
             $referer = $request->headers->get('referer'); 
             return $this->redirect($referer); 
     }
@@ -84,7 +82,6 @@ final class PartyController extends AbstractController
         $party = $slot->getParty();
         if (!$party) {
             $this->addFlash('danger', 'Pas de partie trouvée.');
-            //return $this->redirectToRoute('app_main'); // @todo: à modifier !
             $referer = $request->headers->get('referer'); 
             return $this->redirect($referer);
         }
@@ -98,7 +95,6 @@ final class PartyController extends AbstractController
             if ($party->gamemaster != $gamemasters[0]) {
                 // Alors dégage !
                 $this->addFlash('danger', 'Un MJ ne peut éditer que ses parties.');
-                //return $this->redirectToRoute('app_main'); // @todo: à modifier !
                 $referer = $request->headers->get('referer'); 
                 return $this->redirect($referer);              
             }
@@ -142,13 +138,12 @@ final class PartyController extends AbstractController
                 $manager->persist($party);
                 $manager->flush();
 
-                // @todo: si c'est une partie non validée, envoyer un mail aux admin+gestionnaires pour validation
+                // TODO: si c'est une partie non validée, envoyer un mail aux admin+gestionnaires pour validation
 
                 $this->addFlash('success', 'Partie modifiée.');
             } else {
                 $this->addFlash('danger', 'Pas de MJ ou de jeu sélectionné.');
             }
-            //return $this->redirectToRoute('app_main'); // @todo: à modifier !
             $referer = $request->headers->get('referer'); 
             return $this->redirect($referer);
         }
@@ -226,13 +221,12 @@ final class PartyController extends AbstractController
                 $manager->persist($party);
                 $manager->flush();
 
-                // @todo: si c'est une partie non validée, envoyer un mail aux admin+gestionnaires pour validation
+                // TODO: si c'est une partie non validée, envoyer un mail aux admin+gestionnaires pour validation
 
                 $this->addFlash('success', 'Partie ajoutée au planning.');
             } else {
                 $this->addFlash('danger', 'Pas de MJ ou de jeu sélectionné.');
             }
-            //return $this->redirectToRoute('app_main'); // @todo: à modifier !
             $referer = $request->headers->get('referer'); 
             return $this->redirect($referer);
 

+ 1 - 1
src/Controller/PartyRequestController.php

@@ -66,7 +66,7 @@ final class PartyRequestController extends AbstractController
             $manager->persist($partyRequest);
             $manager->flush();
 
-            // @todo: peut-être envoyer un mail de confirmation ??
+            // TODO: peut-être envoyer un mail de confirmation ??
 
             $this->addFlash('success', 'Demande enregistrée, les gestionnaires y répondront prochainement.');
             $referer = $request->headers->get('referer'); 

+ 28 - 0
src/Controller/PrepareController.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Requirement\Requirement;
+use Symfony\Component\Routing\Attribute\Route;
+
+use App\Repository\EventRepository;
+use App\Entity\Event;
+use App\Entity\Gamemaster;
+
+final class PrepareController extends AbstractController
+{
+    #[Route('/prepare', name: 'app_prepare')]
+    public function prepare(): Response
+    {
+        // Faut être au moins du STAFF
+        $this->denyAccessUnlessGranted('ROLE_STAFF');
+
+        // Tous événement même non publiés à venir
+
+        return $this->render('prepare/index.html.twig', [
+            'controller_name' => 'PrepareController',
+        ]);
+    }
+}

+ 1 - 1
src/Form/ChangePasswordFormType.php

@@ -35,7 +35,7 @@ class ChangePasswordFormType extends AbstractType
                             // max length allowed by Symfony for security reasons
                             'max' => 4096,
                         ]),
-                        // @todo: gérer les deux contraintes suivantes et l'activer
+                        // TODO: gérer les deux contraintes suivantes et l'activer
 //                        new PasswordStrength(),
 //                        new NotCompromisedPassword(),
                     ],

+ 26 - 0
src/Repository/EventRepository.php

@@ -3,6 +3,7 @@
 namespace App\Repository;
 
 use App\Entity\Event;
+use App\Entity\Gamemaster;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
 
@@ -44,6 +45,31 @@ class EventRepository extends ServiceEntityRepository
         return $query->getResult();
     }
 
+    /**
+     * @return Event[]
+     */
+    public function findEventsToPrepare(?Gamemaster $gamemaster): array
+    {
+        // Et il faut la date du moment
+        $dateNow = new \DateTime('now');
+
+        $qb = $this->createQueryBuilder('e')
+            ->where('e.published = :published')
+            ->andWhere('e.endOn > :dateNow')
+            ->setParameter('published', true)
+            ->setParameter('dateNow', $dateNow);
+
+        if ($gamemaster) {
+            $qb->andWhere(':gamemaster MEMBER OF e.gamemastersAssigned')
+            ->setParameter('gamemaster', $gamemaster);
+        }
+        
+        $query = $qb->orderBy('e.startOn', 'ASC')
+           ->getQuery();
+
+        return $query->getResult();
+    }
+
 
     //    /**
     //     * @return Event[] Returns an array of Event objects

+ 1 - 1
templates/manage/booking.html.twig

@@ -67,7 +67,7 @@
             <li><a href="{{ path('app_manage_planning', {id: event.id}) }}">Planning</a></li>
             <li><a href="{{ path('app_manage_party_list', {id: event.id})}}">Liste des parties</a></li>
             <li class="is-active"><a>Liste des participant(e)s</a></li>
-            <li><a>Liste des demandes</a></li>
+            {% if event.isEveryoneCanAskForGame %}<li><a  href="{{ path('app_manage_request', {id: event.id}) }}">Liste des demandes</a></li>{% endif %}
 
 
         </ul>

+ 1 - 1
templates/manage/list.html.twig

@@ -67,7 +67,7 @@
             <li><a href="{{ path('app_manage_planning', {id: event.id}) }}">Planning</a></li>
             <li class="is-active"><a>Liste des parties</a></li>
             <li><a href="{{ path('app_manage_booking', {id: event.id}) }}">Liste des participant(e)s</a></li>
-            <li><a>Liste des demandes</a></li>
+            {% if event.isEveryoneCanAskForGame %}<li><a  href="{{ path('app_manage_request', {id: event.id}) }}">Liste des demandes</a></li>{% endif %}
 
 
         </ul>

+ 1 - 1
templates/manage/manage.html.twig

@@ -67,7 +67,7 @@
             <li class="is-active"><a>Planning</a></li>
             <li><a href="{{ path('app_manage_party_list', {id: event.id})}}">Liste des parties</a></li>
             <li><a href="{{ path('app_manage_booking', {id: event.id}) }}">Liste des participant(e)s</a></li>
-            <li><a>Liste des demandes</a></li>
+            {% if event.isEveryoneCanAskForGame %}<li><a href="{{ path('app_manage_request', {id: event.id}) }}">Liste des demandes</a></li>{% endif %}
 
 
         </ul>

+ 104 - 0
templates/manage/request.html.twig

@@ -0,0 +1,104 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Gestion pour {{ event.name }}{% endblock %}
+
+{% block content %}
+
+{{ component('Modal')}}
+
+
+<div class="card" {{ stimulus_controller('dropdown')}} >
+  <div class="card-footer">
+    <div class="dropdown card-footer-item is-flex is-justify-content-space-between is-align-items-center" data-action="click->dropdown#toggle">
+      <button>
+        <span><strong>{{ event.name }} - du {{ event.startOn|date('d/m/y à H:i', app_timezone) }} au {{ event.endOn|date('d/m/y à H:i', app_timezone)}}</strong></span>        
+      </button>
+      {% if events|length > 1 %}
+      <span class="icon is-small">
+        <twig:ux:icon name="bi:chevron-down" />&nbsp;
+      </span>
+      <div data-dropdown-target="menu" class="dropdown-menu">
+        <div class="dropdown-content">
+          {% for evt in events %}
+          {% if event.id != evt.id %}
+          <a href="{{ path('app_manage_planning', {id: evt.id})}}" class="dropdown-item"><strong>{{ evt.name }}</strong><small> du {{ evt.startOn|date('d/m/y H:i', app_timezone) }} au {{ evt.endOn|date('d/m/y H:i', app_timezone)}}</small></a>
+          {% endif %}
+          {% endfor %}
+        </div>
+      </div>
+      {% endif %}
+    </div>
+  </div>
+</div>
+
+<div class="box is-clearfix">
+  <div class="columns">
+    {% if not event.isHiddenPlanning %}
+    <div class="column">
+      <p>{{ event.getParties()|length }} parties proposées</p>
+    </div>
+    {% endif %}
+    <div class="column">
+      <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="{{ path('app_gamemaster_public_profile', {id: gamemaster.id}) }}" class="open-modal" title="{{ gamemaster.preferedName }}"><figure class="image is-24x24 is-inline-block">
+              {% if gamemaster.picture %}
+              <img class="is-rounded" src="/images/gamemasters/{{ gamemaster.picture }}" alt="{{ gamemaster.preferedName }}"/>
+              {% else %}
+              <twig:ux:icon name="bi:person-fill"/>
+              {% endif %}
+              </figure></a>
+          </span>   
+        {% endfor %}
+      </p>
+    </div>
+  </div>
+
+</div>
+
+<div class="tabs is-boxed">
+    <ul>
+        <li><a href="{{ path('app_manage_planning', {id: event.id}) }}">Planning</a></li>
+        <li><a href="{{ path('app_manage_party_list', {id: event.id}) }}">Liste des parties</a></li>
+        <li><a href="{{ path('app_manage_booking', {id: event.id}) }}">Liste des participant(e)s</a></li>
+        {% if event.isEveryoneCanAskForGame %}<li class="is-active"><a>Liste des demandes</a></li>{% endif %}
+    </ul>
+</div>
+
+
+<section>
+        <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
+            <thead>
+            <tr>
+                <th>Demandeur(euse)</th>
+                <th>Message</th>
+                <th>Jeu</th>
+                <th>Meneur(euse)</th>
+                <th>État</th>
+                <th>Action</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for pRequest in event.getPartyRequests %}
+                <tr>
+                    <th>{{ pRequest.requester.fullName }}</th>
+                    <td>{{ pRequest.message }}</td>
+                    <td>{{ pRequest.gameChoosen.name }}</td>
+                    <td>{{ pRequest.gamemasterChoosen.preferedName }}</td>
+                    <td>{{ pRequest.state['name'] }}</td>
+                    <td>
+                      {% if pRequest.state['code'] == 'WAIT' %}
+                      <a href="{{ path('app_manage_request_accept', {id: pRequest.id}) }}" class="button is-primary">Traiter</a>
+                      <a href="#" data-id="{{ path('app_manage_request_refuse', {id: pRequest.id}) }}" class="button is-warning"  {{ stimulus_controller('admin_confirm') }}>Refuser</a>
+                      {% endif %}
+                      {% if pRequest.state['code'] != 'WAIT' %}<a href="#" data-id="{{ path('app_manage_request_delete', {id: pRequest.id}) }}" class="button is-danger" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>{% endif %}
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+</section>
+
+{% endblock %}

+ 0 - 0
templates/manage/request/accepted.email.html.twig


+ 0 - 0
templates/manage/request/accepted.email.txt.twig


+ 174 - 0
templates/manage/request/process.html.twig

@@ -0,0 +1,174 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Gestion pour {{ event.name }}{% endblock %}
+
+{% block content %}
+
+<div class="card" {{ stimulus_controller('dropdown')}} >
+  <div class="card-footer">
+    <div class="dropdown card-footer-item is-flex is-justify-content-space-between is-align-items-center" data-action="click->dropdown#toggle">
+      <button>
+        <span><strong>{{ event.name }} - du {{ event.startOn|date('d/m/y à H:i', app_timezone) }} au {{ event.endOn|date('d/m/y à H:i', app_timezone)}}</strong></span>        
+      </button>
+      {% if events|length > 1 %}
+      <span class="icon is-small">
+        <twig:ux:icon name="bi:chevron-down" />&nbsp;
+      </span>
+      <div data-dropdown-target="menu" class="dropdown-menu">
+        <div class="dropdown-content">
+          {% for evt in events %}
+          {% if event.id != evt.id %}
+          <a href="{{ path('app_manage_planning', {id: evt.id})}}" class="dropdown-item"><strong>{{ evt.name }}</strong><small> du {{ evt.startOn|date('d/m/y H:i', app_timezone) }} au {{ evt.endOn|date('d/m/y H:i', app_timezone)}}</small></a>
+          {% endif %}
+          {% endfor %}
+        </div>
+      </div>
+      {% endif %}
+    </div>
+  </div>
+</div>
+
+<div class="box is-clearfix">
+  <div class="columns">
+    {% if not event.isHiddenPlanning %}
+    <div class="column">
+      <p>{{ event.getParties()|length }} parties proposées</p>
+    </div>
+    {% endif %}
+    <div class="column">
+      <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="{{ path('app_gamemaster_public_profile', {id: gamemaster.id}) }}" class="open-modal" title="{{ gamemaster.preferedName }}"><figure class="image is-24x24 is-inline-block">
+              {% if gamemaster.picture %}
+              <img class="is-rounded" src="/images/gamemasters/{{ gamemaster.picture }}" alt="{{ gamemaster.preferedName }}"/>
+              {% else %}
+              <twig:ux:icon name="bi:person-fill"/>
+              {% endif %}
+              </figure></a>
+          </span>   
+        {% endfor %}
+      </p>
+    </div>
+  </div>
+</div>
+
+<div class="tabs is-boxed">
+    <ul>
+        <li><a href="{{ path('app_manage_planning', {id: event.id}) }}">Planning</a></li>
+        <li><a href="{{ path('app_manage_party_list', {id: event.id}) }}">Liste des parties</a></li>
+        <li><a href="{{ path('app_manage_booking', {id: event.id}) }}">Liste des participant(e)s</a></li>
+        {% if event.isEveryoneCanAskForGame %}<li class="is-active"><a>Liste des demandes</a></li>{% endif %}
+    </ul>
+</div>
+
+<section>
+    <div clas="block">
+        <div class="content">
+            <h3 class="title is-3">Traiter la demande</h3>
+            <p class="subtitle is-5">Résumer de la demande à traiter</p>
+            <p>Suivez chaque étape, vous pouvez revenir en arrière à tout moment pour sélectionner un autre créneau dans le planning. Scrollez vers le bas pour la suite.</p>
+        </div>
+        <div class="box">
+            <div class="columns">
+                <div class="column is-one-third has-text-right">
+                    <strong>Demandeur(euse)</strong>
+                </div>
+                <div class="column">
+                    {{ partyRequest.requester.fullName }}
+                </div>
+            </div>
+            <div class="columns">
+                <div class="column is-one-third has-text-right">
+                    <strong>Jeu demandé</strong>
+                </div>
+                <div class="column">
+                    {{ partyRequest.gameChoosen.name }}
+                </div>
+            </div>
+            <div class="columns">
+                <div class="column is-one-third has-text-right">
+                    <strong>Meneur(euse) demandé(e)</strong>
+                </div>
+                <div class="column">
+                    {{ partyRequest.gamemasterChoosen.preferedName }}
+                </div>
+            </div>
+            <div class="columns">
+                <div class="column is-one-third has-text-right">
+                    <strong>Message</strong>
+                </div>
+                <div class="column">
+                    {{ partyRequest.message }}
+                </div>
+            </div>
+        </div>
+        <div class="content has-text-centered">
+            <p><span class="has-text-danger"><em>Attention, cette version de l'application ne prend pas encore en charge la disponibilité des MJ ! Vous devez vous assurer de la disponibilité en contrôlant le planning.</em></span></p>
+            <a class="button is-primary" href="#findSlot">Trouver un créneau</a>
+            <a class="button is-warning" href="#" data-id="{{ path('app_manage_request_refuse', {id: partyRequest.id}) }}"  {{ stimulus_controller('admin_confirm') }}>Refuser</a>
+        </div>
+    </div>
+    <hr class="jump-page" />
+</section>
+
+<section {{ stimulus_controller('request_process') }}>
+    <a name="findSlot" id="findSlot"></a>
+    <div clas="block">
+        <div class="content">
+            <h3 class="title is-3">Trouver un créneau libre</h3>
+            <p class="subtitle is-5">Sélectionner le créneau pour la partie demandée.</p>
+            <p>Au pied du planning, vous disposez d'un bouton pour refuser si cette demande ne peut pas être honorée. Cliquez sur le créneau pour passer à l'étape suivante.</p>
+            <p><span class="has-text-danger"><em>Attention, cette version de l'application ne prend pas encore en charge la disponibilité des MJ ! Vous devez vous assurer de la disponibilité en contrôlant le planning.</em></span></p>
+        </div>
+        
+        {{ component('Planning', {event: partyRequest.event, pathEmptySlot: 'app_manage', displayUnvalidates: true}) }}
+    </div>
+    <div class="content has-text-centered">
+        <p>Aucun créneau disponible ne convient ?</p>
+        <a class="button is-warning" href="#" data-id="{{ path('app_manage_request_refuse', {id: partyRequest.id}) }}"  {{ stimulus_controller('admin_confirm') }}>Refuser</a>
+    </div>
+    <hr class="jump-page" />
+</section>
+
+<section>
+    <a name="lastStep" id="lastStep"></a>
+    <div clas="block">
+        <div class="content">
+            <h3 class="title is-3">Finaliser la demande</h3>
+            <p class="subtitle is-5">Sélectionner l'horaire de fin.</p>
+            <p><span class="has-text-danger"><em>Attention, cette version de l'application ne prend pas encore en charge la disponibilité des MJ ! Vous devez vous assurer de la disponibilité en contrôlant le planning.</em></span></p>
+        </div>
+        
+        <div class="content">
+            {{ form_errors(form) }}
+            {{ form_start(form) }}
+            <div class="box">
+                <div class="columns">
+                    <div class="column">
+                        <div class="field">
+                            <label class="label">Horaire de début</label>
+                            <a href="#findSlot" id="party_start"></a>
+                            <small class="help"></small>Cliquez sur la date pour changer l'horaire de début et choisissez un autre créneau.</small>
+                        </div>
+                    </div>
+                    <div class="column">
+                        <div class="field">
+                            <label class="label">Horaire de fin</label>
+                            <select id="party_slots" name="party_slots" class="input">
+                            </select>
+                        </div>
+                    </div>
+                </div>       
+            </div>
+            {{ form_widget(form) }}
+            <div class="control has-text-centered">
+                <button class="button is-primary" type="submit">Envoyer</button>
+                <a href="#" data-id="{{ path('app_manage_request_refuse', {id: partyRequest.getId}) }}" class="button is-warning" data-turbo="false" {{ stimulus_controller('admin_confirm') }}>Refuser</a>
+            </div>
+            {{ form_end(form) }}
+        </div>
+    </div>
+</section>
+
+{% endblock %}

+ 35 - 0
templates/manage/request/refused.email.html.twig

@@ -0,0 +1,35 @@
+{% extends 'base.email.html.twig' %}
+
+{% block title %}En réponse à votre demande de partie pour {{ partyRequest.event.name }}{% endblock %}
+{% block content %}
+                    <tr>
+                        <td style="padding: 0 20px 10px 20px; font-size: 16px;">
+                            <p style="margin: 0;">Bonjour {{ partyRequest.requester.fullName }},</p>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px; font-size: 16px;">
+                            <p style="margin: 0;">Nous sommes au regret de vous annoncer que nous n'avons pas eu la possibilité d'inscrire votre demande au planning des parties de cet événement.</p>
+                            <p style="margin: 0;">Vous pouvez néanmoins vous inscrire aux parties publiées ou faire une demande pour une autre partie.</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>{{ partyRequest.event.name }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Jeu :</strong></td>
+                                    <td>{{ partyRequest.getGameChoosen.name }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Meneur(euse) de jeu :</strong></td>
+                                    <td>{{ partyRequest.getGamemasterChoosen.preferedName }}</td>
+                                </tr>
+                            </table>
+                        </td>
+                    </tr>
+
+{% endblock %}

+ 8 - 0
templates/manage/request/refused.email.txt.twig

@@ -0,0 +1,8 @@
+Bonjour {{ partyRequest.requester.fullName }},
+
+Nous sommes au regret de vous annoncer que nous n'avons pas eu la possibilité d'inscrire votre demande au planning des parties de cet événement.
+Vous pouvez néanmoins vous inscrire aux parties publiées ou faire une demande pour une autre partie.
+
+- Événement : {{ partyRequest.event.name }}
+- Jeu : {{ partyRequest.getGameChoosen.name }}
+- Date : {{ partyRequest.getGamemasterChoosen.preferedName }}

+ 84 - 0
templates/prepare/prepare.html.twig

@@ -0,0 +1,84 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Préparation pour {{ event.name }}{% endblock %}
+
+{% block content %}
+
+{{ component('Modal')}}
+
+
+<div class="card" {{ stimulus_controller('dropdown')}} >
+  <div class="card-footer">
+    <div class="dropdown card-footer-item is-flex is-justify-content-space-between is-align-items-center" data-action="click->dropdown#toggle">
+      <button>
+        <span><strong>{{ event.name }} - du {{ event.startOn|date('d/m/y à H:i', app_timezone) }} au {{ event.endOn|date('d/m/y à H:i', app_timezone)}}</strong></span>        
+      </button>
+      {% if events|length > 1 %}
+      <span class="icon is-small">
+        <twig:ux:icon name="bi:chevron-down" />&nbsp;
+      </span>
+      <div data-dropdown-target="menu" class="dropdown-menu">
+        <div class="dropdown-content">
+          {% for evt in events %}
+          {% if event.id != evt.id %}
+          <a href="{{ path('app_manage_planning', {id: evt.id})}}" class="dropdown-item"><strong>{{ evt.name }}</strong><small> du {{ evt.startOn|date('d/m/y H:i', app_timezone) }} au {{ evt.endOn|date('d/m/y H:i', app_timezone)}}</small></a>
+          {% endif %}
+          {% endfor %}
+        </div>
+      </div>
+      {% endif %}
+    </div>
+  </div>
+
+
+
+
+
+</div>
+
+<div class="box is-clearfix">
+  <div class="columns">
+    {% if not event.isHiddenPlanning %}
+    <div class="column">
+      <p>{{ event.getParties()|length }} parties proposées</p>
+    </div>
+    {% endif %}
+    <div class="column">
+      <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="{{ path('app_gamemaster_public_profile', {id: gamemaster.id}) }}" class="open-modal" title="{{ gamemaster.preferedName }}"><figure class="image is-24x24 is-inline-block">
+              {% if gamemaster.picture %}
+              <img class="is-rounded" src="/images/gamemasters/{{ gamemaster.picture }}" alt="{{ gamemaster.preferedName }}"/>
+              {% else %}
+              <twig:ux:icon name="bi:person-fill"/>
+              {% endif %}
+              </figure></a>
+          </span>   
+        {% endfor %}
+      </p>
+    </div>
+  </div>
+
+</div>
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li class="is-active"><a>Planning</a></li>
+            <li><a>Disponibilités</a></li>
+        </ul>
+    </div>
+
+
+<section>
+  <div id="planning">
+    {% if is_granted('ROLE_MANAGER') %}
+    {{ component('Planning', {event: event, pathEmptySlot: 'app_party_add', pathFullSlot: 'app_party_modify', displayUnvalidates: true}) }}
+    {% else %}
+    N'joute que les parties de MJ (à supprimer, c'est d debug)
+    {{ component('Planning', {event: event, pathEmptySlot: 'app_party_add', displayUnvalidates: true}) }}
+    {% endif %}
+  </div>
+</section>
+
+{% endblock %}