浏览代码

premier jet création des parties et affichage du planning

garthh 2 天之前
父节点
当前提交
b778aab79c

+ 2 - 1
.env

@@ -47,4 +47,5 @@ APP_TZ="Europe/Paris"
 CONTACT_EMAIL=no-reply@mail.com
 CONTACT_NAME=Orgasso
 APP_ALLOW_REGISTER=true
-APP_ALLOW_PUBLIC_EVENT=true
+APP_DEFAULT_MIN_PARTICIPANTS=1
+APP_DEFAULT_MAX_PARTICIPANTS=5

+ 37 - 0
assets/controllers/party_selector_controller.js

@@ -0,0 +1,37 @@
+import { Controller } from '@hotwired/stimulus';
+
+/*
+ * Contrôleur Stimulus pour le formulaire JEUX.
+ */
+
+export default class extends Controller {
+
+    connect() {
+        this.initDisabling();
+        console.log("Stimulus: gestion des désactivation d'options dans les parties");
+        
+    }
+
+initDisabling() {
+    const checkInGamemaster = document.querySelector('#gamemaster-controller select');
+
+    checkInGamemaster.addEventListener('change', () => {
+        const selectedOption = checkInGamemaster.options[checkInGamemaster.selectedIndex];
+        const gamemasterID = checkInGamemaster.value;
+        const gamemasterCanMaster = selectedOption.dataset.games
+            ? selectedOption.dataset.games.split("|")
+            : [];
+
+        const gamesToRefresh = Array.from(document.querySelectorAll("#game-controller option"));
+
+        gamesToRefresh.forEach((el) => {
+            if (!gamemasterID) {
+                el.disabled = true;
+            } else {
+                el.disabled = !gamemasterCanMaster.includes(el.value);
+            }
+        });
+    });
+}
+
+}

+ 31 - 2
assets/styles/app.css

@@ -93,10 +93,10 @@ body.is-dark-mode .planning-cell {
   white-space: nowrap;
   text-overflow: ellipsis;
   text-align: center;
-  border-bottom: 1px solid #dbdbdb;
   /*background-color: #ffffff;*/
   font-size: 0.95rem;
   transition: background-color 0.2s ease;
+  z-index: 0;
 }
 
 /* Header cells (e.g., "Espaces") */
@@ -105,6 +105,7 @@ body.is-dark-mode .planning-cell {
   /* color: #ffffff;*/
   font-weight: 600;
   border-bottom: 1px solid #00b89c;
+  z-index: 0;
 }
 
 /* Wide header cells (e.g., time columns) */
@@ -113,6 +114,7 @@ body.is-dark-mode .planning-cell {
   color: #363636;
   font-weight: 500;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
 }
 
 /* Free (available) slot */
@@ -120,6 +122,7 @@ body.is-dark-mode .planning-cell {
   background-color: #effaf5;     /* Bulma success-light */
   color: #0f8763;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
 }
 
 .planning-cell-free:hover {
@@ -131,6 +134,7 @@ body.is-dark-mode .planning-cell {
   background-color: #dbdbdb; /* Bulma gray */
   color: #7a7a7a;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
 }
 
 /* Hidden slot */
@@ -138,9 +142,34 @@ body.is-dark-mode .planning-cell {
   background-color: transparent;
   color: transparent;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
+}
+
+.planning-cell-game-parent {
+  position: relative;
+  overflow: visible;
+  border: none !important;
+  z-index: 0 !important;
+}
+
+.planning-cell-game {
+  border: none;
+  width: 98%;
+  color: black;
+  position: absolute;
+  top: 0rem;
+  left: 0rem;
+  z-index: 99 !important;
+  white-space: normal;
+  text-overflow: clip;
+  overflow: hidden;
 }
 
 /* Optional: highlight on hover globally */
-.planning-cell:hover:not(.planning-cell-hidden):not(.planning-cell-heading):not(.planning-cell-wide) {
+.planning-cell:hover:not(.planning-cell-hidden):not(.planning-cell-heading):not(.planning-cell-wide):not(.planning-cell-game):not(.planning-cell-game-parent) {
   filter: brightness(0.98);
+}
+
+.planning-cell-game .card {
+  border-left: 3px solid blue;
 }

+ 43 - 0
migrations/Version20250803035835.php

@@ -0,0 +1,43 @@
+<?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 Version20250803035835 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 party (id INT AUTO_INCREMENT NOT NULL, gamemaster_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', game_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', event_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', start_on DATETIME DEFAULT NULL, end_on DATETIME DEFAULT NULL, gamemaster_is_author TINYINT(1) DEFAULT NULL, min_participants INT DEFAULT NULL, max_participants INT DEFAULT NULL, INDEX IDX_89954EE096376157 (gamemaster_id), INDEX IDX_89954EE0E48FD905 (game_id), INDEX IDX_89954EE071F7E88B (event_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE party ADD CONSTRAINT FK_89954EE096376157 FOREIGN KEY (gamemaster_id) REFERENCES gamemaster (id)');
+        $this->addSql('ALTER TABLE party ADD CONSTRAINT FK_89954EE0E48FD905 FOREIGN KEY (game_id) REFERENCES game (id)');
+        $this->addSql('ALTER TABLE party ADD CONSTRAINT FK_89954EE071F7E88B FOREIGN KEY (event_id) REFERENCES event (id)');
+        $this->addSql('ALTER TABLE slot ADD party_id INT DEFAULT NULL');
+        $this->addSql('ALTER TABLE slot ADD CONSTRAINT FK_AC0E2067213C1059 FOREIGN KEY (party_id) REFERENCES party (id)');
+        $this->addSql('CREATE INDEX IDX_AC0E2067213C1059 ON slot (party_id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE slot DROP FOREIGN KEY FK_AC0E2067213C1059');
+        $this->addSql('ALTER TABLE party DROP FOREIGN KEY FK_89954EE096376157');
+        $this->addSql('ALTER TABLE party DROP FOREIGN KEY FK_89954EE0E48FD905');
+        $this->addSql('ALTER TABLE party DROP FOREIGN KEY FK_89954EE071F7E88B');
+        $this->addSql('DROP TABLE party');
+        $this->addSql('DROP INDEX IDX_AC0E2067213C1059 ON slot');
+        $this->addSql('ALTER TABLE slot DROP party_id');
+    }
+}

+ 31 - 0
migrations/Version20250803040519.php

@@ -0,0 +1,31 @@
+<?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 Version20250803040519 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('ALTER TABLE party ADD description LONGTEXT DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE party DROP description');
+    }
+}

+ 35 - 0
migrations/Version20250803064511.php

@@ -0,0 +1,35 @@
+<?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 Version20250803064511 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('ALTER TABLE party ADD submitter_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', ADD submitted_date DATETIME DEFAULT NULL, ADD validated TINYINT(1) DEFAULT NULL');
+        $this->addSql('ALTER TABLE party ADD CONSTRAINT FK_89954EE0919E5513 FOREIGN KEY (submitter_id) REFERENCES user (id)');
+        $this->addSql('CREATE INDEX IDX_89954EE0919E5513 ON party (submitter_id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE party DROP FOREIGN KEY FK_89954EE0919E5513');
+        $this->addSql('DROP INDEX IDX_89954EE0919E5513 ON party');
+        $this->addSql('ALTER TABLE party DROP submitter_id, DROP submitted_date, DROP validated');
+    }
+}

二进制
public/images/events/placeholder.webp-dist


二进制
public/images/games/placeholder.webp-dist


+ 22 - 0
src/Controller/Admin/GamemasterController.php

@@ -5,6 +5,7 @@ namespace App\Controller\Admin;
 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;
@@ -172,4 +173,25 @@ final class GamemasterController extends AbstractController
             'gamemaster' => $gamemaster,
         ]);
     }
+
+    /*
+     * API Json : checker si un MJ peut mener un jeu ! 
+    
+    #[Route('/api/gm-capablity', name: 'app_api_gm_capability', methods: ['GEt'])]
+    public function gmCapability(Request $request, GamemasterRepository $repository): JsonResponse
+    {
+        $data = json_decode($request->getContent(), true);
+        $gamemasterID = $data['gamemasterId'] ?? null;
+
+        if (!$gamemasterID) {
+            return new JsonResponse(['success' => false]);
+        }
+
+        // retrouver le MJ
+        $gamemaster = $repository->findByStrID($gamemasterID);
+        $canMaster = array_map(fn($g) => $g->getId(), $gamemaster->getGamesCanMaster()->toArray());
+
+        return new JsonResponse(['success' => true, 'canMaster' => $canMaster]);
+
+    } */
 }

+ 108 - 0
src/Controller/PartyController.php

@@ -0,0 +1,108 @@
+<?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 App\Entity\Slot;
+use App\Entity\Party;
+use App\Entity\Gamemaster;
+use App\Entity\Game;
+use App\Entity\Event;
+use App\Repository\SlotRepository;
+use App\Repository\GamemasterRepository;
+use App\Repository\GameRepository;
+use App\Form\PartyType;
+
+use App\Security\Voter\SlotAccessVoter;
+
+final class PartyController extends AbstractController
+{
+    #[Route('/party', name: 'app_party')]
+    public function index(): Response
+    {
+        return $this->render('party/index.html.twig', [
+            'controller_name' => 'PartyController',
+        ]);
+    }
+
+    #[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
+    {
+        // Seuls gestionnaires (MANAGER), admin (ADMIN) ou un MJ de l'asso (STAFF) qui est associé à l'événement
+        $this->denyAccessUnlessGranted(SlotAccessVoter::ACCESS_SLOT, $slot);
+
+        $user = $this->getUser();
+        // Création de l'objet minimaliste
+        $party = new Party();
+        $party->setEvent($slot->getEvent());
+        $party->setSubmitter($user);
+        $party->setSubmittedDate(new \Datetime('now'));
+        $party->setMinParticipants($_ENV['APP_DEFAULT_MIN_PARTICIPANTS']);
+        $party->setMaxParticipants($_ENV['APP_DEFAULT_MAX_PARTICIPANTS']);
+        $roles = $user->getRoles();
+
+        if (in_array('ROLE_ADMIN', $roles) || in_array('ROLE_MANAGER', $roles)) {
+            $party->setValidated(true);
+            $gamemasters = $slot->getEvent()->getGamemastersAssigned();
+            
+        } else {
+            // Note, le rôle admin et/ou manager prend le pas sur le rôle Staff/MJ
+            $party->setValidated(false);
+            $gamemasters = array();
+            $gamemasters[] = $user->getLinkToGamemaster();
+        }
+
+        // Création des valeurs dispo pour les formulaire
+        $games = $slot->getEvent()->getGameAssigned();
+        $getSlotsAvailables = $slotRepository->findNextsAvailables($slot);
+        $slotStart = $slot;
+        $slotsAvailables = array_merge([$slotStart], $getSlotsAvailables);
+
+        $form = $this->createForm(PartyType::class, $party);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            // Ajouter les slots séléctionnés
+            $slotsString = $request->request->get('party_slots');
+            $gamemasterSelectedId = $request->request->get('party_gamemaster');
+            $gamemasterSelected = $gamemasterRepository->findByStrID($gamemasterSelectedId);
+            $gameSelectedId = $request->request->get('party_game');
+            $gameSelected = $gameRepository->findByStrID($gameSelectedId);
+            $new_slots = explode("|", $slotsString);
+            foreach($new_slots as $add_this_slot) {
+                $add_this_slot_obj = $slotRepository->findById($add_this_slot);
+                $party->addSlot($add_this_slot_obj);
+            }
+            $party->setStartOn(clone $party->getSlots()[0]->getStartOn());
+            $party->setEndOn(clone $party->getSlots()->last()->getEndOn());
+
+
+
+            $party->setGamemaster($gamemasterSelected);
+            $party->setGame($gameSelected);
+
+            $manager->persist($party);
+            $manager->flush();
+
+            $this->addFlash('success', 'Partie ajoutée au planning');
+            return $this->redirectToRoute('app_main'); // @todo: à modifier !
+
+        }
+
+        return $this->render('party/edit.html.twig', [
+            'form' => $form,
+            'gamemasters' => $gamemasters,
+            'games' => $games,
+            'slotStart' => $slotStart,
+            'slotsAvailables' => $slotsAvailables,
+        ]);
+    }
+
+
+}

+ 37 - 0
src/Entity/Event.php

@@ -83,6 +83,12 @@ class Event
     #[ORM\ManyToMany(targetEntity: Game::class, inversedBy: 'eventsAssignedTo')]
     private Collection $gameAssigned;
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'event', orphanRemoval: true)]
+    private Collection $parties;
+
     public function __construct()
     {
         $this->spaces = new ArrayCollection();
@@ -90,6 +96,7 @@ class Event
         $this->slots = new ArrayCollection();
         $this->gamemastersAssigned = new ArrayCollection();
         $this->gameAssigned = new ArrayCollection();
+        $this->parties = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -366,4 +373,34 @@ class Event
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Party>
+     */
+    public function getParties(): Collection
+    {
+        return $this->parties;
+    }
+
+    public function addParty(Party $party): static
+    {
+        if (!$this->parties->contains($party)) {
+            $this->parties->add($party);
+            $party->setEvent($this);
+        }
+
+        return $this;
+    }
+
+    public function removeParty(Party $party): static
+    {
+        if ($this->parties->removeElement($party)) {
+            // set the owning side to null (unless already changed)
+            if ($party->getEvent() === $this) {
+                $party->setEvent(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 37 - 0
src/Entity/Game.php

@@ -77,12 +77,19 @@ class Game
     #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'gameAssigned')]
     private Collection $eventsAssignedTo;
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'game')]
+    private Collection $parties;
+
     public function __construct()
     {
         $this->genre = new ArrayCollection();
         $this->id = Uuid::v7();
         $this->gamemasters = new ArrayCollection();
         $this->eventsAssignedTo = new ArrayCollection();
+        $this->parties = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -331,4 +338,34 @@ class Game
         return $this;
     }
 
+    /**
+     * @return Collection<int, Party>
+     */
+    public function getParties(): Collection
+    {
+        return $this->parties;
+    }
+
+    public function addParty(Party $party): static
+    {
+        if (!$this->parties->contains($party)) {
+            $this->parties->add($party);
+            $party->setGame($this);
+        }
+
+        return $this;
+    }
+
+    public function removeParty(Party $party): static
+    {
+        if ($this->parties->removeElement($party)) {
+            // set the owning side to null (unless already changed)
+            if ($party->getGame() === $this) {
+                $party->setGame(null);
+            }
+        }
+
+        return $this;
+    }
+
 }

+ 37 - 0
src/Entity/Gamemaster.php

@@ -66,10 +66,17 @@ class Gamemaster
     #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'gamemastersAssigned')]
     private Collection $eventsAssignedTo;
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'gamemaster')]
+    private Collection $parties;
+
     public function __construct()
     {
         $this->gamesCanMaster = new ArrayCollection();
         $this->eventsAssignedTo = new ArrayCollection();
+        $this->parties = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -258,5 +265,35 @@ class Gamemaster
         return $this;
     }
 
+    /**
+     * @return Collection<int, Party>
+     */
+    public function getParties(): Collection
+    {
+        return $this->parties;
+    }
+
+    public function addParty(Party $party): static
+    {
+        if (!$this->parties->contains($party)) {
+            $this->parties->add($party);
+            $party->setGamemaster($this);
+        }
+
+        return $this;
+    }
+
+    public function removeParty(Party $party): static
+    {
+        if ($this->parties->removeElement($party)) {
+            // set the owning side to null (unless already changed)
+            if ($party->getGamemaster() === $this) {
+                $party->setGamemaster(null);
+            }
+        }
+
+        return $this;
+    }
+
 
 }

+ 246 - 0
src/Entity/Party.php

@@ -0,0 +1,246 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\PartyRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity(repositoryClass: PartyRepository::class)]
+class Party
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'parties')]
+    private ?Gamemaster $gamemaster = null;
+
+    #[ORM\ManyToOne(inversedBy: 'parties')]
+    private ?Game $game = null;
+
+    /**
+     * @var Collection<int, Slot>
+     */
+    #[ORM\OneToMany(targetEntity: Slot::class, mappedBy: 'party')]
+    #[ORM\OrderBy(["startOn" => "ASC"])]
+    private Collection $slots;
+
+    #[ORM\ManyToOne(inversedBy: 'parties')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Event $event = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?\DateTime $startOn = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?\DateTime $endOn = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $gamemasterIsAuthor = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?int $minParticipants = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?int $maxParticipants = null;
+
+    #[ORM\Column(type: Types::TEXT, nullable: true)]
+    private ?string $description = null;
+
+    #[ORM\ManyToOne(inversedBy: 'submittedParties')]
+    private ?User $submitter = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?\DateTime $submittedDate = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $validated = null;
+
+    public function __construct()
+    {
+        $this->slots = new ArrayCollection();
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getGamemaster(): ?Gamemaster
+    {
+        return $this->gamemaster;
+    }
+
+    public function setGamemaster(?Gamemaster $gamemaster): static
+    {
+        $this->gamemaster = $gamemaster;
+
+        return $this;
+    }
+
+    public function getGame(): ?Game
+    {
+        return $this->game;
+    }
+
+    public function setGame(?Game $game): static
+    {
+        $this->game = $game;
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, Slot>
+     */
+    public function getSlots(): Collection
+    {
+        return $this->slots;
+    }
+
+    public function addSlot(Slot $slot): static
+    {
+        if (!$this->slots->contains($slot)) {
+            $this->slots->add($slot);
+            $slot->setParty($this);
+        }
+
+        return $this;
+    }
+
+    public function removeSlot(Slot $slot): static
+    {
+        if ($this->slots->removeElement($slot)) {
+            // set the owning side to null (unless already changed)
+            if ($slot->getParty() === $this) {
+                $slot->setParty(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getEvent(): ?Event
+    {
+        return $this->event;
+    }
+
+    public function setEvent(?Event $event): static
+    {
+        $this->event = $event;
+
+        return $this;
+    }
+
+    public function getStartOn(): ?\DateTime
+    {
+        return $this->startOn;
+    }
+
+    public function setStartOn(?\DateTime $startOn): static
+    {
+        $this->startOn = $startOn;
+
+        return $this;
+    }
+
+    public function getEndOn(): ?\DateTime
+    {
+        return $this->endOn;
+    }
+
+    public function setEndOn(?\DateTime $endOn): static
+    {
+        $this->endOn = $endOn;
+
+        return $this;
+    }
+
+    public function isGamemasterIsAuthor(): ?bool
+    {
+        return $this->gamemasterIsAuthor;
+    }
+
+    public function setGamemasterIsAuthor(?bool $gamemasterIsAuthor): static
+    {
+        $this->gamemasterIsAuthor = $gamemasterIsAuthor;
+
+        return $this;
+    }
+
+    public function getMinParticipants(): ?int
+    {
+        return $this->minParticipants;
+    }
+
+    public function setMinParticipants(?int $minParticipants): static
+    {
+        $this->minParticipants = $minParticipants;
+
+        return $this;
+    }
+
+    public function getMaxParticipants(): ?int
+    {
+        return $this->maxParticipants;
+    }
+
+    public function setMaxParticipants(?int $maxParticipants): static
+    {
+        $this->maxParticipants = $maxParticipants;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(?string $description): static
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getSubmitter(): ?User
+    {
+        return $this->submitter;
+    }
+
+    public function setSubmitter(?User $submitter): static
+    {
+        $this->submitter = $submitter;
+
+        return $this;
+    }
+
+    public function getSubmittedDate(): ?\DateTime
+    {
+        return $this->submittedDate;
+    }
+
+    public function setSubmittedDate(?\DateTime $submittedDate): static
+    {
+        $this->submittedDate = $submittedDate;
+
+        return $this;
+    }
+
+    public function isValidated(): ?bool
+    {
+        return $this->validated;
+    }
+
+    public function setValidated(?bool $validated): static
+    {
+        $this->validated = $validated;
+
+        return $this;
+    }
+}

+ 15 - 0
src/Entity/Slot.php

@@ -34,6 +34,9 @@ class Slot
     #[ORM\Column(nullable: true)]
     private ?bool $unavailable = null;
 
+    #[ORM\ManyToOne(inversedBy: 'slots')]
+    private ?Party $party = null;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -111,4 +114,16 @@ class Slot
         return $this;
     }
 
+    public function getParty(): ?Party
+    {
+        return $this->party;
+    }
+
+    public function setParty(?Party $party): static
+    {
+        $this->party = $party;
+
+        return $this;
+    }
+
 }

+ 37 - 0
src/Entity/User.php

@@ -73,10 +73,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\OneToOne(mappedBy: 'linkToUser')]
     private ?Gamemaster $linkToGamemaster = null;
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'submitter')]
+    private Collection $submittedParties;
+
     public function __construct()
     {
         $this->lastUpdate = new \DateTime('now');
         $this->games = new ArrayCollection();
+        $this->submittedParties = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -340,4 +347,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Party>
+     */
+    public function getSubmittedParties(): Collection
+    {
+        return $this->submittedParties;
+    }
+
+    public function addSubmittedParty(Party $submittedParty): static
+    {
+        if (!$this->submittedParties->contains($submittedParty)) {
+            $this->submittedParties->add($submittedParty);
+            $submittedParty->setSubmitter($this);
+        }
+
+        return $this;
+    }
+
+    public function removeSubmittedParty(Party $submittedParty): static
+    {
+        if ($this->submittedParties->removeElement($submittedParty)) {
+            // set the owning side to null (unless already changed)
+            if ($submittedParty->getSubmitter() === $this) {
+                $submittedParty->setSubmitter(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 2 - 0
src/Form/GameType.php

@@ -115,6 +115,7 @@ class GameType extends AbstractType
                 'label' => 'Date de création de la fiche',
                 'label_attr' => ['class' => 'label'],
                 'attr' => ['class' => 'input'],
+                'view_timezone' => $_ENV['APP_TZ'],
                 'required' => true,
                 'row_attr' => ['class' => 'field'],
                 'help_attr' => ['class' => 'help'],
@@ -131,6 +132,7 @@ class GameType extends AbstractType
                 'label' => 'Date de validation de la fiche',
                 'label_attr' => ['class' => 'label'],
                 'attr' => ['class' => 'input'],
+                'view_timezone' => $_ENV['APP_TZ'],
                 'required' => false,
                 'row_attr' => ['class' => 'field'],
                 'help_attr' => ['class' => 'help'],

+ 91 - 0
src/Form/PartyType.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Event;
+use App\Entity\Game;
+use App\Entity\Gamemaster;
+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;
+use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
+
+class PartyType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            /*->add('gamemaster', EntityType::class, [
+                'class' => Gamemaster::class,
+                'choice_label' => 'preferedName',
+                'label' => 'Meneur(euse) de jeu',
+                'attr' => ['class' => 'input'],
+                'required' => false,
+                'expanded' => false,
+                'multiple' => false,
+                'label_attr' => ['class' => 'label'],
+                'choice_attr' => ['class' => 'select'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('game', EntityType::class, [
+                'class' => Game::class,
+                'choice_label' => 'name',
+                'label' => 'Jeu',
+                'attr' => ['class' => 'input'],
+                'required' => false,
+                'expanded' => false,
+                'multiple' => false,
+                'label_attr' => ['class' => 'label'],
+                'choice_attr' => ['class' => 'select'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ]) */
+            ->add('description', null, [
+                'label' => 'Description',
+                'label_attr' => ['class' => 'label'],
+                'help' => 'Description sommaire de la partie.',
+                'attr' => ['class' => 'textarea',
+                           'rows' => 6],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('gamemasterIsAuthor', null, [
+                'label' => 'le(a) MJ est l\'auteur(rice) du jeu',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si par partie est animée par l\'auteur(rice) du jeu.',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('minParticipants', null, [
+                'label' => 'Nombre minimum de participant(e)s',
+                'help' => 'Nombre minimum de participant(e)s pour permettre de jouer la partie.',
+                'required' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],               
+            ])
+            ->add('maxParticipants', null, [
+                'label' => 'Nombre maximum de participant(e)s',
+                'help' => 'Nombre maximum de participant(e)s pouvant jouer cette partie.',
+                'required' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],               
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Party::class,
+        ]);
+    }
+}

+ 16 - 0
src/Repository/GameRepository.php

@@ -5,6 +5,7 @@ namespace App\Repository;
 use App\Entity\Game;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
+use Symfony\Component\Uid\UuidV7;
 
 /**
  * @extends ServiceEntityRepository<Game>
@@ -25,6 +26,21 @@ class GameRepository extends ServiceEntityRepository
             ->getResult();
     }
 
+    public function findByStrID(string $id): ?Game
+    {
+        if (!UuidV7::isValid($id)) {
+            return null;
+        }
+
+        try {
+            $uuid = UuidV7::fromString($id);
+        } catch (\Throwable $e) {
+            return null;
+        }
+
+        return $this->find($uuid);
+    }
+
     //    /**
     //     * @return Genre[] Returns an array of Genre objects
     //     */

+ 43 - 0
src/Repository/PartyRepository.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Party;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Party>
+ */
+class PartyRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Party::class);
+    }
+
+//    /**
+//     * @return Party[] Returns an array of Party 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): ?Party
+//    {
+//        return $this->createQueryBuilder('p')
+//            ->andWhere('p.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}

+ 24 - 0
src/Repository/SlotRepository.php

@@ -57,6 +57,30 @@ class SlotRepository extends ServiceEntityRepository
         return $qb->getOneOrNullResult();
     }
 
+    public function findNextsAvailables(Slot $slot): array 
+    {
+        $results = [];
+
+        $nextSlot = $this->findNext($slot);
+
+        while ($nextSlot && is_null($nextSlot->getParty()) && !$nextSlot->isUnavailable()) {
+            $results[] = $nextSlot;
+            $nextSlot = $this->findNext($nextSlot);
+        }
+
+        return $results;
+    }
+
+    public function findById(int $id): ?Slot
+    {
+        $qb = $this->createQueryBuilder('s')
+            ->where('s.id = :id')
+            ->setParameter('id', $id)
+            ->getQuery();
+        
+        return $qb->getOneOrNullResult();
+    }
+
 
     //    /**
     //     * @return Slot[] Returns an array of Slot objects

+ 55 - 0
src/Security/Voter/SlotAccessVoter.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Security\Voter;
+
+use App\Entity\Slot;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\User\UserInterface;
+
+class SlotAccessVoter extends Voter
+{
+    public const ACCESS_SLOT = 'ACCESS_SLOT';
+
+    protected function supports(string $attribute, $subject): bool
+    {
+        return $attribute === self::ACCESS_SLOT && $subject instanceof Slot;
+    }
+
+    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
+    {
+        $user = $token->getUser();
+
+        if (!$user instanceof UserInterface) {
+            return false;
+        }
+
+        $roles = $user->getRoles();
+
+        // Accès immédiat pour ADMIN ou MANAGER
+        if (in_array('ROLE_ADMIN', $roles, true) || in_array('ROLE_MANAGER', $roles, true)) {
+            return true;
+        }
+
+        // Vérifie les droits pour ROLE_STAFF
+        if (in_array('ROLE_STAFF', $roles, true)) {
+            $gamemaster = $user->getLinkToGamemaster();
+
+            if (!$gamemaster) {
+                return false;
+            }
+
+            $event = $subject->getEvent();
+            if (!$event) {
+                return false;
+            }
+
+            $assignedGamemasters = $event->getGamemasterAssigned();
+
+            return in_array($gamemaster, $assignedGamemasters->toArray(), true);
+        }
+
+        // ❌ Aucun autre rôle n'est autorisé
+        return false;
+    }
+}

+ 31 - 30
templates/admin/event/config/game.html.twig

@@ -45,39 +45,40 @@
         <h3 class="title is-3">Assignez les jeux</h3>
         <p>Cliquez sur les jeux à supprimer de cet événement.  <em class="has-text-danger">Fonctionnalité en cours de développement.</em></p>
       </div>
-
-      <div class="grid is-col-min-12">
-        {% for game in gamesPlayed %}
-
-        <div class="cell">
-          
-          <div class="card">
-            <div class="card-image">
-            <figure class="image is-3by1">
-
-                {% if game.picture %}
-                <img src="/images/games/{{ game.picture }}"  />
-                {% else %}
-                
-                {% endif %}
-
-            </figure>
-            </div>
-            <div class="card-content">
-              <div class="content text-limit-height">
-                <p>
-                  <strong>{{ game.name }}</strong>
-                  <br/>
-                  {% for genre in game.genre %}<span class="tag is-info is-light">{{ genre.genre }}</span> {% endfor %}
-                  
-                  
-                </p>
+      <div class="fixed-grid has-6-cols-fullhd has-6-cols-widescreen has-4-cols-desktop has-4-cols-tablet has-2-cols-mobile">
+        <div class="grid is-col-min-12">
+          {% for game in gamesPlayed %}
+
+          <div class="cell">
+            
+            <div class="card">
+              <div class="card-image">
+              <figure class="image is-3by1">
+
+                  {% if game.picture %}
+                  <img src="/images/games/{{ game.picture }}"  />
+                  {% else %}
+                  <img src="/images/games/placeholder.webp"  />
+                  {% endif %}
+
+              </figure>
+              </div>
+              <div class="card-content">
+                <div class="content text-limit-height">
+                  <p>
+                    <strong>{{ game.name }}</strong>
+                    <br/>
+                    {% for genre in game.genre %}<span class="tag is-info is-light">{{ genre.genre }}</span> {% endfor %}
+                    
+                    
+                  </p>
+                </div>
               </div>
             </div>
+            
           </div>
-          
+          {% endfor %}
         </div>
-        {% endfor %}
       </div>
 
             <div class="content">
@@ -97,7 +98,7 @@
                 {% if game.picture %}
                 <img src="/images/games/{{ game.picture }}"  />
                 {% else %}
-                
+                <img src="/images/games/placeholder.webp"  />
                 {% endif %}
 
             </figure>

+ 10 - 0
templates/admin/event/config/index.html.twig

@@ -83,6 +83,16 @@
                 <p class="heading has-text-danger">Aucun jeu</p>
               {% endif %}
             </div>
+            <div class="level-item has-text-centered">
+              {% if event.getParties()|length > 0 %}
+              <div>
+                <p class="title">{{ event.getParties()|length }}</p>
+                <p class="heading">parties</p>
+                </div>
+              {% else %}
+                <p class="heading has-text-danger">Aucune partie</p>
+              {% endif %}
+            </div>
           </div>
         </div>
       </div>

+ 10 - 1
templates/admin/event/config/party.html.twig

@@ -40,9 +40,18 @@
                   La liste des jeux de rôle doit être déterminée pour enregistrer des parties sur le planning de cet événement.
               </div>
           </article>
+    {% elseif event.getSlots|length < 1 %}
+            <article class="message is-danger">
+              <div class="message-header">
+                <p>Aucun planning générez</p>
+              </div>
+              <div class="message-body">
+                  Générez le planning en générant les slots avant d'inscrire les parties.
+              </div>
+          </article>
     {% else %}
         <div id="planning">
-          {{ component('Planning', {event: event, pathEmptySlot: 'app_main', pathFullSlot: 'app_main'}) }}
+          {{ component('Planning', {event: event, pathEmptySlot: 'app_party_add', pathFullSlot: 'app_main'}) }}
         </div>
     {% endif %}
 

+ 1 - 1
templates/admin/gamemaster/index.html.twig

@@ -20,7 +20,7 @@
     <div class="block">
         <div class="is-grouped">
             <a class="button is-primary" href="{{ path('app_admin_gamemaster_add') }}">Ajouter un(e) meneur(euse) de jeu</a>
-            <a class="button" href="{{ path('app_admin_gamemaster_link') }}">Associer les MJ à leurs comptes</a>
+            <a class="button" data-turbo="false" href="{{ path('app_admin_gamemaster_link') }}">Associer les MJ à leurs comptes</a>
         </div>
     </div>
 

+ 40 - 5
templates/components/Planning.html.twig

@@ -36,7 +36,7 @@
                         {# si le slot est Indisponible #}
                         {% if thisSlot.unavailable %}
                             {% if displayLocked %}
-                            <div class="cell planning-cell planning-cell-locked" data-id="thisSlot.id">
+                            <div class="cell planning-cell planning-cell-locked" data-id="{{ thisSlot.id }}">
                                 <div class="icon"><twig:ux:icon name="bi:lock-fill" /></div>
                             </div>
                             {% else %}
@@ -45,17 +45,52 @@
                             {% endif %}
                         {% endif %}
                         {# si une partie est sur le slot #}
-                        {# TODO : à compléter quand les parties seront ajoutées #}
+                        {% if thisSlot.party  %}
+                            {% if thisSlot == thisSlot.party.slots[0] and thisSlot.party.isValidated %}
+                                {# Premier slot d'une partie ou partie non validée #}
+                                <div class="cell planning-cell planning-cell-game-parent">
+                                    <div class="planning-cell-game" style="height: {{ thisSlot.party.slots|length * 3 }}rem !important">
+                                      {# Carte "jeu" DEBUT #}
+                                        <div class="card" style="height: {{ thisSlot.party.slots|length * 3 }}rem !important">
+                                            {% if pathFullSlot %}<a href="{{ path(pathFullSlot, {id: thisSlot.id}) }}">{% endif %}
+                                                <div class="card-header">
+                                                    {{ thisSlot.party.game.name }}
+                                                </div>
+                                                <div class="card-image">
+                                                    <figure class="image is-3by1">
+                                                        {% if thisSlot.party.game.picture %}
+                                                        <img src="/images/games/{{ thisSlot.party.game.picture }}"  />
+                                                        {% else %}
+                                                        <img src="/images/games/placeholder.webp"  />
+                                                        {% endif %}
+                                                    </figure>
+                                                </div>
+                                                <div class="card-content">
+                                                    <div class="content">
+                                                    
+                                                    </div>
+                                                </div>
+                                            {% if pathFullSlot %}</a>{% endif %}
+                                        </div>                             
+                                      {# Carte "jeu" FIN #}
+                                    </div>
+                                </div>
+                            {% else %}
+                                {# Slot suivants d'une partie #}
+                                <div class="cell planning-cell planning-cell-game-parent">
+                                </div>
+                            {% endif %}
+                        {% endif %}
                         {# si le slot est disponible et sans partie #}
-                        {% if not thisSlot.unavailable %}
+                        {% if not thisSlot.unavailable and not thisSlot.party %}
                             {% if pathEmptySlot %}
                             <a href="{{ path(pathEmptySlot, {id: thisSlot.id}) }}">
-                            <div class="cell planning-cell planning-cell-free" data-id="thisSlot.id">
+                            <div class="cell planning-cell planning-cell-free" data-id="{{ thisSlot.id }}">
                                 <div class="icon"><twig:ux:icon name="bi:plus-circle" /></div>
                             </div>
                             </a>
                             {% else %}
-                            <div class="cell planning-cell planning-cell-free" data-id="thisSlot.id">
+                            <div class="cell planning-cell planning-cell-free" data-id="{{ thisSlot.id }}">
                                 
                             </div>                            
                             {% endif %}

+ 83 - 0
templates/party/edit.html.twig

@@ -0,0 +1,83 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Ajouter une partie{% endblock %}
+
+{% block content %}
+
+    
+
+    {{ form_errors(form) }}
+    {{ form_start(form) }}
+
+    <div id="gamemaster-controller" class="field" {{ stimulus_controller('party_selector') }}>
+        <label class="label">Meneur(euse) de jeu</label>
+        <select id="party_gamemaster" name="party_gamemaster" class="input">
+            <option value=""></option>
+            {% for gamemaster in gamemasters %}
+            <option value="{{ gamemaster.id }}" data-games="{{ gamemaster.getGamesCanMaster|map(game => game.getId)|join('|') }}">{{ gamemaster.getPreferedName()|capitalize }}</option> 
+            {% endfor %}
+        </select>
+    </div>
+
+    <div id="game-controller" class="field">
+        <label class="label">Jeu de rôle</label>
+        <select id="party_game" name="party_game" class="input">
+            <option value=""></option>
+            {% for game in games %}
+            <option value="{{ game.id }}" disabled>{{ game.getName() }}</option> 
+            {% endfor %}
+        </select>
+    </div>
+    
+    <div class="field">
+        {{ form_widget(form.gamemasterIsAuthor) }}
+        {{ form_label(form.gamemasterIsAuthor) }}
+        {{ form_help(form.gamemasterIsAuthor) }}
+    </div>
+    {{ form_row(form.description) }}
+
+    <div class="box">
+        <div class="columns">
+            <div class="column">
+                {{ form_row(form.minParticipants) }}
+            </div>
+            <div class="column">
+                {{ form_row(form.maxParticipants) }}
+            </div>
+        </div>
+    </div>
+
+    <div class="box">
+         <div class="columns">
+            <div class="column">
+                <div class="field">
+                    <label class="label">Horaire de début</label>
+                    <select id="party_start_slot" name="party_start_slot" class="input">
+                        <option value="{{ slotStart.id }}">{{ slotStart.startOn|date('d/m/Y H:i', app_timezone) }}</option>
+                    </select>
+                </div>
+            </div>
+            <div class="column">
+                <div class="field">
+                    <label class="label">Horaire de fin</label>
+                    <select id="party_slots" name="party_slots" class="input">
+                    {% set SlotC = [] %}
+                    {% for slot in slotsAvailables %}
+                        {% set SlotC = SlotC|merge([slot.id]) %}
+                        <option value="{{ SlotC|join("|") }}">{{ slot.endOn|date('d/m/Y H:i', app_timezone) }}</option>
+                    {% endfor %}
+                    </select>
+                </div>
+            </div>
+        </div>       
+    </div>
+
+    {{ form_widget(form) }}
+    
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+    </div>
+
+    {{ form_end(form) }}
+
+{% endblock %}

+ 20 - 0
templates/party/index.html.twig

@@ -0,0 +1,20 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Hello PartyController!{% endblock %}
+
+{% block body %}
+<style>
+    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
+    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
+</style>
+
+<div class="example-wrapper">
+    <h1>Hello {{ controller_name }}! ✅</h1>
+
+    This friendly message is coming from:
+    <ul>
+        <li>Your controller at <code>/Users/garthh/Developpement/orgasso/src/Controller/PartyController.php</code></li>
+        <li>Your template at <code>/Users/garthh/Developpement/orgasso/templates/party/index.html.twig</code></li>
+    </ul>
+</div>
+{% endblock %}

+ 1 - 0
templates/profile/gameadd.html.twig

@@ -16,6 +16,7 @@
             <li ><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
             {% if app.user.linkToGamemaster %}
             <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
+            <li><a>Disponibilités MJ</a></li>
             <li><a href="{{ path('app_profile_gamelist') }}">Ludothèque</a></li>
             <li class="is-active"><a>Proposer un jeu</a></li>
             {% endif %}

+ 1 - 0
templates/profile/gamelist.html.twig

@@ -16,6 +16,7 @@
             <li ><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
             {% if app.user.linkToGamemaster %}
             <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
+            <li><a>Disponibilités MJ</a></li>
             <li class="is-active"><a>Ludothèque</a></li>
             <li><a href="{{ path('app_profile_gameadd') }}">Proposer un jeu</a></li>
             {% endif %}

+ 1 - 0
templates/profile/gamemaster.html.twig

@@ -16,6 +16,7 @@
             <li><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
             {% if app.user.linkToGamemaster %}
             <li class="is-active"><a>Meneur(euse) de jeu</a></li>
+            <li><a>Disponibilités MJ</a></li>
             <li><a href="{{ path('app_profile_gamelist')}}">Ludothèque</a></li>
             <li><a href="{{ path('app_profile_gameadd') }}">Proposer un jeu</a></li>
             {% endif %}

+ 1 - 0
templates/profile/index.html.twig

@@ -16,6 +16,7 @@
             <li class="is-active"><a>Compte utilisateur(rice)</a></li>
             {% if app.user.linkToGamemaster %}
             <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
+            <li><a>Disponibilités MJ</a></li>
             <li><a href="{{ path('app_profile_gamelist')}}">Ludothèque</a></li>
             <li><a href="{{ path('app_profile_gameadd') }}">Proposer un jeu</a></li>
             {% endif %}