Эх сурвалжийг харах

premier jet création des parties et affichage du planning

garthh 2 өдөр өмнө
parent
commit
b778aab79c
33 өөрчлөгдсөн 1148 нэмэгдсэн , 40 устгасан
  1. 2 1
      .env
  2. 37 0
      assets/controllers/party_selector_controller.js
  3. 31 2
      assets/styles/app.css
  4. 43 0
      migrations/Version20250803035835.php
  5. 31 0
      migrations/Version20250803040519.php
  6. 35 0
      migrations/Version20250803064511.php
  7. BIN
      public/images/events/placeholder.webp-dist
  8. BIN
      public/images/games/placeholder.webp-dist
  9. 22 0
      src/Controller/Admin/GamemasterController.php
  10. 108 0
      src/Controller/PartyController.php
  11. 37 0
      src/Entity/Event.php
  12. 37 0
      src/Entity/Game.php
  13. 37 0
      src/Entity/Gamemaster.php
  14. 246 0
      src/Entity/Party.php
  15. 15 0
      src/Entity/Slot.php
  16. 37 0
      src/Entity/User.php
  17. 2 0
      src/Form/GameType.php
  18. 91 0
      src/Form/PartyType.php
  19. 16 0
      src/Repository/GameRepository.php
  20. 43 0
      src/Repository/PartyRepository.php
  21. 24 0
      src/Repository/SlotRepository.php
  22. 55 0
      src/Security/Voter/SlotAccessVoter.php
  23. 31 30
      templates/admin/event/config/game.html.twig
  24. 10 0
      templates/admin/event/config/index.html.twig
  25. 10 1
      templates/admin/event/config/party.html.twig
  26. 1 1
      templates/admin/gamemaster/index.html.twig
  27. 40 5
      templates/components/Planning.html.twig
  28. 83 0
      templates/party/edit.html.twig
  29. 20 0
      templates/party/index.html.twig
  30. 1 0
      templates/profile/gameadd.html.twig
  31. 1 0
      templates/profile/gamelist.html.twig
  32. 1 0
      templates/profile/gamemaster.html.twig
  33. 1 0
      templates/profile/index.html.twig

+ 2 - 1
.env

@@ -47,4 +47,5 @@ APP_TZ="Europe/Paris"
 CONTACT_EMAIL=no-reply@mail.com
 CONTACT_EMAIL=no-reply@mail.com
 CONTACT_NAME=Orgasso
 CONTACT_NAME=Orgasso
 APP_ALLOW_REGISTER=true
 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;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   text-align: center;
   text-align: center;
-  border-bottom: 1px solid #dbdbdb;
   /*background-color: #ffffff;*/
   /*background-color: #ffffff;*/
   font-size: 0.95rem;
   font-size: 0.95rem;
   transition: background-color 0.2s ease;
   transition: background-color 0.2s ease;
+  z-index: 0;
 }
 }
 
 
 /* Header cells (e.g., "Espaces") */
 /* Header cells (e.g., "Espaces") */
@@ -105,6 +105,7 @@ body.is-dark-mode .planning-cell {
   /* color: #ffffff;*/
   /* color: #ffffff;*/
   font-weight: 600;
   font-weight: 600;
   border-bottom: 1px solid #00b89c;
   border-bottom: 1px solid #00b89c;
+  z-index: 0;
 }
 }
 
 
 /* Wide header cells (e.g., time columns) */
 /* Wide header cells (e.g., time columns) */
@@ -113,6 +114,7 @@ body.is-dark-mode .planning-cell {
   color: #363636;
   color: #363636;
   font-weight: 500;
   font-weight: 500;
   border-bottom: 1px solid #ccc;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
 }
 }
 
 
 /* Free (available) slot */
 /* Free (available) slot */
@@ -120,6 +122,7 @@ body.is-dark-mode .planning-cell {
   background-color: #effaf5;     /* Bulma success-light */
   background-color: #effaf5;     /* Bulma success-light */
   color: #0f8763;
   color: #0f8763;
   border-bottom: 1px solid #ccc;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
 }
 }
 
 
 .planning-cell-free:hover {
 .planning-cell-free:hover {
@@ -131,6 +134,7 @@ body.is-dark-mode .planning-cell {
   background-color: #dbdbdb; /* Bulma gray */
   background-color: #dbdbdb; /* Bulma gray */
   color: #7a7a7a;
   color: #7a7a7a;
   border-bottom: 1px solid #ccc;
   border-bottom: 1px solid #ccc;
+  z-index: 0;
 }
 }
 
 
 /* Hidden slot */
 /* Hidden slot */
@@ -138,9 +142,34 @@ body.is-dark-mode .planning-cell {
   background-color: transparent;
   background-color: transparent;
   color: transparent;
   color: transparent;
   border-bottom: 1px solid #ccc;
   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 */
 /* 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);
   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');
+    }
+}

BIN
public/images/events/placeholder.webp-dist


BIN
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\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\Routing\Requirement\Requirement;
 use Symfony\Component\Routing\Requirement\Requirement;
 use Symfony\Component\Routing\Attribute\Route;
 use Symfony\Component\Routing\Attribute\Route;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\EntityManagerInterface;
@@ -172,4 +173,25 @@ final class GamemasterController extends AbstractController
             'gamemaster' => $gamemaster,
             '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')]
     #[ORM\ManyToMany(targetEntity: Game::class, inversedBy: 'eventsAssignedTo')]
     private Collection $gameAssigned;
     private Collection $gameAssigned;
 
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'event', orphanRemoval: true)]
+    private Collection $parties;
+
     public function __construct()
     public function __construct()
     {
     {
         $this->spaces = new ArrayCollection();
         $this->spaces = new ArrayCollection();
@@ -90,6 +96,7 @@ class Event
         $this->slots = new ArrayCollection();
         $this->slots = new ArrayCollection();
         $this->gamemastersAssigned = new ArrayCollection();
         $this->gamemastersAssigned = new ArrayCollection();
         $this->gameAssigned = new ArrayCollection();
         $this->gameAssigned = new ArrayCollection();
+        $this->parties = new ArrayCollection();
     }
     }
 
 
     public function getId(): ?Uuid
     public function getId(): ?Uuid
@@ -366,4 +373,34 @@ class Event
 
 
         return $this;
         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')]
     #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'gameAssigned')]
     private Collection $eventsAssignedTo;
     private Collection $eventsAssignedTo;
 
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'game')]
+    private Collection $parties;
+
     public function __construct()
     public function __construct()
     {
     {
         $this->genre = new ArrayCollection();
         $this->genre = new ArrayCollection();
         $this->id = Uuid::v7();
         $this->id = Uuid::v7();
         $this->gamemasters = new ArrayCollection();
         $this->gamemasters = new ArrayCollection();
         $this->eventsAssignedTo = new ArrayCollection();
         $this->eventsAssignedTo = new ArrayCollection();
+        $this->parties = new ArrayCollection();
     }
     }
 
 
     public function getId(): ?Uuid
     public function getId(): ?Uuid
@@ -331,4 +338,34 @@ class Game
         return $this;
         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')]
     #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'gamemastersAssigned')]
     private Collection $eventsAssignedTo;
     private Collection $eventsAssignedTo;
 
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'gamemaster')]
+    private Collection $parties;
+
     public function __construct()
     public function __construct()
     {
     {
         $this->gamesCanMaster = new ArrayCollection();
         $this->gamesCanMaster = new ArrayCollection();
         $this->eventsAssignedTo = new ArrayCollection();
         $this->eventsAssignedTo = new ArrayCollection();
+        $this->parties = new ArrayCollection();
     }
     }
 
 
     public function getId(): ?Uuid
     public function getId(): ?Uuid
@@ -258,5 +265,35 @@ class Gamemaster
         return $this;
         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)]
     #[ORM\Column(nullable: true)]
     private ?bool $unavailable = null;
     private ?bool $unavailable = null;
 
 
+    #[ORM\ManyToOne(inversedBy: 'slots')]
+    private ?Party $party = null;
+
     public function getId(): ?int
     public function getId(): ?int
     {
     {
         return $this->id;
         return $this->id;
@@ -111,4 +114,16 @@ class Slot
         return $this;
         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')]
     #[ORM\OneToOne(mappedBy: 'linkToUser')]
     private ?Gamemaster $linkToGamemaster = null;
     private ?Gamemaster $linkToGamemaster = null;
 
 
+    /**
+     * @var Collection<int, Party>
+     */
+    #[ORM\OneToMany(targetEntity: Party::class, mappedBy: 'submitter')]
+    private Collection $submittedParties;
+
     public function __construct()
     public function __construct()
     {
     {
         $this->lastUpdate = new \DateTime('now');
         $this->lastUpdate = new \DateTime('now');
         $this->games = new ArrayCollection();
         $this->games = new ArrayCollection();
+        $this->submittedParties = new ArrayCollection();
     }
     }
 
 
     public function getId(): ?Uuid
     public function getId(): ?Uuid
@@ -340,4 +347,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
 
 
         return $this;
         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' => 'Date de création de la fiche',
                 'label_attr' => ['class' => 'label'],
                 'label_attr' => ['class' => 'label'],
                 'attr' => ['class' => 'input'],
                 'attr' => ['class' => 'input'],
+                'view_timezone' => $_ENV['APP_TZ'],
                 'required' => true,
                 'required' => true,
                 'row_attr' => ['class' => 'field'],
                 'row_attr' => ['class' => 'field'],
                 'help_attr' => ['class' => 'help'],
                 'help_attr' => ['class' => 'help'],
@@ -131,6 +132,7 @@ class GameType extends AbstractType
                 'label' => 'Date de validation de la fiche',
                 'label' => 'Date de validation de la fiche',
                 'label_attr' => ['class' => 'label'],
                 'label_attr' => ['class' => 'label'],
                 'attr' => ['class' => 'input'],
                 'attr' => ['class' => 'input'],
+                'view_timezone' => $_ENV['APP_TZ'],
                 'required' => false,
                 'required' => false,
                 'row_attr' => ['class' => 'field'],
                 'row_attr' => ['class' => 'field'],
                 'help_attr' => ['class' => 'help'],
                 '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 App\Entity\Game;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
 use Doctrine\Persistence\ManagerRegistry;
+use Symfony\Component\Uid\UuidV7;
 
 
 /**
 /**
  * @extends ServiceEntityRepository<Game>
  * @extends ServiceEntityRepository<Game>
@@ -25,6 +26,21 @@ class GameRepository extends ServiceEntityRepository
             ->getResult();
             ->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
     //     * @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();
         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
     //     * @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>
         <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>
         <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>
-
-      <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>
             </div>
+            
           </div>
           </div>
-          
+          {% endfor %}
         </div>
         </div>
-        {% endfor %}
       </div>
       </div>
 
 
             <div class="content">
             <div class="content">
@@ -97,7 +98,7 @@
                 {% if game.picture %}
                 {% if game.picture %}
                 <img src="/images/games/{{ game.picture }}"  />
                 <img src="/images/games/{{ game.picture }}"  />
                 {% else %}
                 {% else %}
-                
+                <img src="/images/games/placeholder.webp"  />
                 {% endif %}
                 {% endif %}
 
 
             </figure>
             </figure>

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

@@ -83,6 +83,16 @@
                 <p class="heading has-text-danger">Aucun jeu</p>
                 <p class="heading has-text-danger">Aucun jeu</p>
               {% endif %}
               {% endif %}
             </div>
             </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>
         </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.
                   La liste des jeux de rôle doit être déterminée pour enregistrer des parties sur le planning de cet événement.
               </div>
               </div>
           </article>
           </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 %}
     {% else %}
         <div id="planning">
         <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>
         </div>
     {% endif %}
     {% endif %}
 
 

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

@@ -20,7 +20,7 @@
     <div class="block">
     <div class="block">
         <div class="is-grouped">
         <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 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>
     </div>
     </div>
 
 

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

@@ -36,7 +36,7 @@
                         {# si le slot est Indisponible #}
                         {# si le slot est Indisponible #}
                         {% if thisSlot.unavailable %}
                         {% if thisSlot.unavailable %}
                             {% if displayLocked %}
                             {% 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 class="icon"><twig:ux:icon name="bi:lock-fill" /></div>
                             </div>
                             </div>
                             {% else %}
                             {% else %}
@@ -45,17 +45,52 @@
                             {% endif %}
                             {% endif %}
                         {% endif %}
                         {% endif %}
                         {# si une partie est sur le slot #}
                         {# 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 #}
                         {# si le slot est disponible et sans partie #}
-                        {% if not thisSlot.unavailable %}
+                        {% if not thisSlot.unavailable and not thisSlot.party %}
                             {% if pathEmptySlot %}
                             {% if pathEmptySlot %}
                             <a href="{{ path(pathEmptySlot, {id: thisSlot.id}) }}">
                             <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 class="icon"><twig:ux:icon name="bi:plus-circle" /></div>
                             </div>
                             </div>
                             </a>
                             </a>
                             {% else %}
                             {% 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>                            
                             </div>                            
                             {% endif %}
                             {% 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>
             <li ><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
             {% if app.user.linkToGamemaster %}
             {% if app.user.linkToGamemaster %}
             <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
             <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_gamelist') }}">Ludothèque</a></li>
             <li class="is-active"><a>Proposer un jeu</a></li>
             <li class="is-active"><a>Proposer un jeu</a></li>
             {% endif %}
             {% endif %}

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

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

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

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

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

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