Forráskód Böngészése

génération des spaces, periods et slots

garthh 12 órája
szülő
commit
ef11b151d5
36 módosított fájl, 1914 hozzáadás és 31 törlés
  1. 1 6
      .gitignore
  2. 57 0
      assets/controllers/bulma_tabs_controller.js
  3. 15 0
      assets/icons/orgasso.svg
  4. 31 0
      migrations/Version20250731170906.php
  5. 31 0
      migrations/Version20250731171858.php
  6. 31 0
      migrations/Version20250801072844.php
  7. 45 0
      migrations/Version20250801083732.php
  8. 31 0
      migrations/Version20250801110344.php
  9. 31 0
      migrations/Version20250801123256.php
  10. 0 0
      public/images/events/.gitignore
  11. 0 0
      public/pages/landing.md-dist
  12. 55 0
      src/Controller/Admin/EventConfig/PeriodController.php
  13. 115 0
      src/Controller/Admin/EventConfig/SlotController.php
  14. 55 0
      src/Controller/Admin/EventConfig/SpaceController.php
  15. 158 0
      src/Controller/Admin/EventController.php
  16. 0 3
      src/Controller/Admin/UserController.php
  17. 135 18
      src/Entity/Event.php
  18. 81 0
      src/Entity/Period.php
  19. 113 0
      src/Entity/Slot.php
  20. 66 0
      src/Entity/Space.php
  21. 24 0
      src/Form/ContactType.php
  22. 122 0
      src/Form/EventType.php
  23. 49 0
      src/Form/PeriodType.php
  24. 35 0
      src/Form/SpaceType.php
  25. 43 0
      src/Repository/PeriodRepository.php
  26. 72 0
      src/Repository/SlotRepository.php
  27. 43 0
      src/Repository/SpaceRepository.php
  28. 26 0
      src/Service/DateTimeHelper.php
  29. 103 0
      templates/admin/event/config/period.html.twig
  30. 104 0
      templates/admin/event/config/slot.html.twig
  31. 94 0
      templates/admin/event/config/space.html.twig
  32. 93 0
      templates/admin/event/edit.html.twig
  33. 52 0
      templates/admin/event/index.html.twig
  34. 2 2
      templates/admin/game/index.html.twig
  35. 1 1
      templates/admin/index.html.twig
  36. 0 1
      templates/bulma.html.twig

+ 1 - 6
.gitignore

@@ -20,14 +20,9 @@
 ###< symfony/asset-mapper ###
 
 ### Spécifique au projet ###
-/public/images/gamemasters/*.png
-/public/images/gamemasters/*.jpg
-/public/images/gamemasters/*.jpeg
 /public/images/gamemasters/*.webp
-/public/images/games/*.png
-/public/images/games/*.jpg
-/public/images/games/*.jpeg
 /public/images/games/*.webp
+/public/images/events/*.webp
 /public/pages/*.md
 /database.db
 /some_datas.sql

+ 57 - 0
assets/controllers/bulma_tabs_controller.js

@@ -0,0 +1,57 @@
+import { Controller } from '@hotwired/stimulus';
+
+/*
+ * Contrôleur Stimulus pour la gestion d’onglets.
+ * Affiche le bloc correspondant à l’onglet cliqué et masque les autres.
+ * 
+ * Les TABS sont présentées par une div avec une liste
+ * <div class="tabs is-boxes" {{ stimulus_controller('bulma_tabs') }}>
+ *   <ul>
+ *     <li class="is-active" data-id="tab-1"><a>Tab 1</a></li>
+ *     <li data-id="tab-2"><a>Tab 2</a></li>
+ *   </ul>
+ * </div>
+ * <div id="tabs-content">
+ *    <div class="container" id="tab-1">
+ *      ---- CONTENU TAB 1 ----
+ *    </div>
+ *    <div class="container is-hidden" id="tab-2">
+ *      ---- CONTENU TAB 2 ----
+ *    </div>
+ * </div>
+ */
+
+export default class extends Controller {
+    static targets = ['tab'];
+
+    connect() {
+        console.log("Stimulus: contrôleur d’onglets actif");
+
+        this.tabs = this.element.querySelectorAll('[data-id]');
+        this.containers = document.querySelectorAll('#tabs-content .container');
+
+        this.tabs.forEach(tab => {
+            tab.addEventListener('click', (event) => {
+                event.preventDefault();
+                this.activateTab(tab);
+            });
+        });
+    }
+
+    activateTab(selectedTab) {
+        const targetId = selectedTab.dataset.id;
+
+        // Mise à jour des onglets actifs
+        this.tabs.forEach(tab => tab.classList.remove('is-active'));
+        selectedTab.classList.add('is-active');
+
+        // Affichage du bon bloc
+        this.containers.forEach(container => {
+            if (container.id === targetId) {
+                container.classList.remove('is-hidden');
+            } else {
+                container.classList.add('is-hidden');
+            }
+        });
+    }
+}

+ 15 - 0
assets/icons/orgasso.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(2.74754,0,0,2.15092,-748.376,-447.156)">
+        <path d="M334.903,455.574C315.646,455.574 301.018,449.695 291.018,437.936C281.019,426.178 276.02,409.652 276.02,388.358L276.02,274.756C276.02,254.203 281.019,238.695 291.018,228.233C301.018,217.771 315.646,212.54 334.903,212.54C354.161,212.54 368.789,217.771 378.788,228.233C388.788,238.695 393.787,254.203 393.787,274.756L393.787,388.358C393.787,409.837 388.788,426.41 378.788,438.075C368.789,449.741 354.161,455.574 334.903,455.574ZM334.903,411.689C339.162,411.689 342.032,409.837 343.514,406.134C344.995,402.43 345.736,397.246 345.736,390.58L345.736,273.923C345.736,268.738 345.041,264.526 343.653,261.285C342.264,258.045 339.44,256.425 335.181,256.425C327.219,256.425 323.238,262.443 323.238,274.479L323.238,390.857C323.238,397.709 324.071,402.893 325.738,406.411C327.404,409.93 330.459,411.689 334.903,411.689Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+        <path d="M412.952,214.762L486.279,214.762C497.945,214.762 506.972,217.4 513.36,222.678C519.748,227.955 524.053,235.362 526.275,244.898C528.497,254.434 529.608,266.609 529.608,281.423C529.608,294.94 527.849,305.494 524.331,313.086C520.813,320.678 514.702,325.956 505.999,328.918C513.221,330.4 518.452,334.01 521.692,339.751C524.933,345.491 526.553,353.268 526.553,363.082L525.998,453.352L479.335,453.352L479.335,360.027C479.335,353.361 478.039,349.102 475.447,347.25C472.854,345.398 468.132,344.473 461.281,344.473L461.281,453.352L412.952,453.352L412.952,214.762ZM472.947,303.087C479.613,303.087 482.946,295.866 482.946,281.423C482.946,275.127 482.668,270.405 482.113,267.257C481.557,264.109 480.539,261.933 479.057,260.73C477.576,259.526 475.447,258.924 472.669,258.924L461.559,258.924L461.559,303.087L472.947,303.087Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+        <path d="M594.603,455.574C577.382,455.574 564.559,449.51 556.134,437.381C547.709,425.252 543.496,406.874 543.496,382.247L543.496,279.478C543.496,257.443 548.403,240.778 558.217,229.483C568.031,218.187 582.659,212.54 602.102,212.54C617.841,212.54 630.063,215.688 638.766,221.983C647.468,228.279 653.486,237.445 656.819,249.481C660.153,261.517 661.819,276.978 661.819,295.866L615.156,295.866L615.156,274.479C615.156,268.738 614.37,264.294 612.796,261.147C611.222,257.999 608.398,256.425 604.324,256.425C595.251,256.425 590.714,262.35 590.714,274.201L590.714,389.746C590.714,396.968 591.64,402.43 593.492,406.134C595.343,409.837 598.584,411.689 603.213,411.689C607.842,411.689 611.083,409.837 612.934,406.134C614.786,402.43 615.712,396.968 615.712,389.746L615.712,355.583L602.935,355.583L602.935,314.475L661.264,314.475L661.264,453.352L642.099,453.352L634.044,433.354C625.526,448.167 612.379,455.574 594.603,455.574Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+    </g>
+    <g transform="matrix(2.97077,0,0,3.09252,-367.326,-1032.68)">
+        <path d="M127.013,678.379L143.047,512.434L199.264,512.434L215.105,678.379L183.616,678.379L181.298,651.527L161.206,651.527L159.275,678.379L127.013,678.379ZM163.718,625.06L178.786,625.06L171.445,540.639L169.9,540.639L163.718,625.06Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+        <path d="M264.56,679.925C249.491,679.925 238.609,676.19 231.912,668.72C225.215,661.25 221.866,649.208 221.866,632.595L221.866,616.367L254.514,616.367L254.514,637.038C254.514,645.28 257.154,649.402 262.435,649.402C265.397,649.402 267.457,648.532 268.617,646.794C269.776,645.055 270.355,642.125 270.355,638.004C270.355,632.595 269.711,628.119 268.423,624.577C267.135,621.036 265.493,618.074 263.497,615.691C261.501,613.308 257.927,609.606 252.776,604.583L238.48,590.287C227.404,579.469 221.866,567.492 221.866,554.355C221.866,540.188 225.118,529.402 231.622,521.997C238.126,514.592 247.624,510.889 260.116,510.889C275.056,510.889 285.81,514.849 292.378,522.77C298.946,530.69 302.231,543.086 302.231,559.958L268.423,559.958L268.23,548.56C268.23,546.37 267.618,544.632 266.395,543.344C265.171,542.056 263.465,541.412 261.276,541.412C258.7,541.412 256.768,542.12 255.48,543.537C254.192,544.954 253.548,546.885 253.548,549.332C253.548,554.742 256.639,560.344 262.821,566.139L282.139,584.685C286.647,589.064 290.382,593.217 293.344,597.145C296.306,601.073 298.689,605.71 300.492,611.055C302.295,616.399 303.196,622.742 303.196,630.083C303.196,646.439 300.202,658.835 294.213,667.271C288.225,675.707 278.34,679.925 264.56,679.925Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+        <path d="M353.617,679.925C338.549,679.925 327.666,676.19 320.969,668.72C314.272,661.25 310.924,649.208 310.924,632.595L310.924,616.367L343.572,616.367L343.572,637.038C343.572,645.28 346.212,649.402 351.492,649.402C354.454,649.402 356.515,648.532 357.674,646.794C358.833,645.055 359.413,642.125 359.413,638.004C359.413,632.595 358.769,628.119 357.481,624.577C356.193,621.036 354.551,618.074 352.555,615.691C350.559,613.308 346.985,609.606 341.833,604.583L327.538,590.287C316.462,579.469 310.924,567.492 310.924,554.355C310.924,540.188 314.176,529.402 320.68,521.997C327.183,514.592 336.682,510.889 349.174,510.889C364.114,510.889 374.868,514.849 381.436,522.77C388.004,530.69 391.288,543.086 391.288,559.958L357.481,559.958L357.288,548.56C357.288,546.37 356.676,544.632 355.453,543.344C354.229,542.056 352.523,541.412 350.333,541.412C347.757,541.412 345.826,542.12 344.538,543.537C343.25,544.954 342.606,546.885 342.606,549.332C342.606,554.742 345.697,560.344 351.879,566.139L371.197,584.685C375.705,589.064 379.44,593.217 382.402,597.145C385.364,601.073 387.746,605.71 389.55,611.055C391.353,616.399 392.254,622.742 392.254,630.083C392.254,646.439 389.26,658.835 383.271,667.271C377.282,675.707 367.398,679.925 353.617,679.925Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+        <path d="M442.868,679.925C429.474,679.925 419.3,675.836 412.345,667.657C405.391,659.479 401.913,647.985 401.913,633.174L401.913,554.162C401.913,539.866 405.391,529.08 412.345,521.804C419.3,514.527 429.474,510.889 442.868,510.889C456.262,510.889 466.437,514.527 473.391,521.804C480.346,529.08 483.823,539.866 483.823,554.162L483.823,633.174C483.823,648.114 480.346,659.64 473.391,667.754C466.437,675.868 456.262,679.925 442.868,679.925ZM442.868,649.402C445.83,649.402 447.827,648.114 448.857,645.538C449.887,642.962 450.402,639.356 450.402,634.72L450.402,553.582C450.402,549.976 449.919,547.046 448.953,544.793C447.988,542.539 446.024,541.412 443.061,541.412C437.523,541.412 434.754,545.598 434.754,553.969L434.754,634.913C434.754,639.678 435.334,643.284 436.493,645.731C437.652,648.178 439.777,649.402 442.868,649.402Z" style="fill:rgb(46,48,146);fill-rule:nonzero;"/>
+    </g>
+</svg>

+ 31 - 0
migrations/Version20250731170906.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 Version20250731170906 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 event CHANGE start_on start_on DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', CHANGE end_on end_on DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE event CHANGE start_on start_on DATETIME NOT NULL COMMENT \'(DC2Type:datetimetz_immutable)\', CHANGE end_on end_on DATETIME NOT NULL COMMENT \'(DC2Type:datetimetz_immutable)\'');
+    }
+}

+ 31 - 0
migrations/Version20250731171858.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 Version20250731171858 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 event CHANGE start_on start_on DATETIME NOT NULL, CHANGE end_on end_on DATETIME NOT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE event CHANGE start_on start_on DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', CHANGE end_on end_on DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
+    }
+}

+ 31 - 0
migrations/Version20250801072844.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 Version20250801072844 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 event ADD private TINYINT(1) DEFAULT NULL, ADD hidden_planning TINYINT(1) DEFAULT NULL, DROP public, DROP everyone_can_show_planning');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE event ADD public TINYINT(1) DEFAULT NULL, ADD everyone_can_show_planning TINYINT(1) DEFAULT NULL, DROP private, DROP hidden_planning');
+    }
+}

+ 45 - 0
migrations/Version20250801083732.php

@@ -0,0 +1,45 @@
+<?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 Version20250801083732 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 period (id INT AUTO_INCREMENT NOT NULL, event_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', start_on DATETIME NOT NULL, end_on DATETIME NOT NULL, INDEX IDX_C5B81ECE71F7E88B (event_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE slot (id INT AUTO_INCREMENT NOT NULL, period_id INT NOT NULL, space_id INT NOT NULL, event_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', start_on DATETIME NOT NULL, end_on DATETIME NOT NULL, unavailable TINYINT(1) DEFAULT NULL, INDEX IDX_AC0E2067EC8B7ADE (period_id), INDEX IDX_AC0E206723575340 (space_id), INDEX IDX_AC0E206771F7E88B (event_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE space (id INT AUTO_INCREMENT NOT NULL, event_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', name VARCHAR(255) NOT NULL, INDEX IDX_2972C13A71F7E88B (event_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE period ADD CONSTRAINT FK_C5B81ECE71F7E88B FOREIGN KEY (event_id) REFERENCES event (id)');
+        $this->addSql('ALTER TABLE slot ADD CONSTRAINT FK_AC0E2067EC8B7ADE FOREIGN KEY (period_id) REFERENCES period (id)');
+        $this->addSql('ALTER TABLE slot ADD CONSTRAINT FK_AC0E206723575340 FOREIGN KEY (space_id) REFERENCES space (id)');
+        $this->addSql('ALTER TABLE slot ADD CONSTRAINT FK_AC0E206771F7E88B FOREIGN KEY (event_id) REFERENCES event (id)');
+        $this->addSql('ALTER TABLE space ADD CONSTRAINT FK_2972C13A71F7E88B FOREIGN KEY (event_id) REFERENCES event (id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE period DROP FOREIGN KEY FK_C5B81ECE71F7E88B');
+        $this->addSql('ALTER TABLE slot DROP FOREIGN KEY FK_AC0E2067EC8B7ADE');
+        $this->addSql('ALTER TABLE slot DROP FOREIGN KEY FK_AC0E206723575340');
+        $this->addSql('ALTER TABLE slot DROP FOREIGN KEY FK_AC0E206771F7E88B');
+        $this->addSql('ALTER TABLE space DROP FOREIGN KEY FK_2972C13A71F7E88B');
+        $this->addSql('DROP TABLE period');
+        $this->addSql('DROP TABLE slot');
+        $this->addSql('DROP TABLE space');
+    }
+}

+ 31 - 0
migrations/Version20250801110344.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 Version20250801110344 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 space ADD locked TINYINT(1) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE space DROP locked');
+    }
+}

+ 31 - 0
migrations/Version20250801123256.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 Version20250801123256 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 period ADD locked TINYINT(1) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE period DROP locked');
+    }
+}

+ 0 - 0
public/images/events/.gitignore


+ 0 - 0
public/pages/landing.md-dist


+ 55 - 0
src/Controller/Admin/EventConfig/PeriodController.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Controller\Admin\EventConfig;
+
+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\Event;
+use App\Entity\Period;
+use App\Form\PeriodType;
+use App\Repository\PeriodRepository;
+
+final class PeriodController extends AbstractController
+{
+    #[Route('/admin/event/{id}/configure/period', name: 'app_admin_event_config_period', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function index(?Event $event, Request $request, EntityManagerInterface $manager, PeriodRepository $repository): Response
+    {
+        $period = new Period();
+        $period->setEvent($event);
+        
+        $form = $this->createForm(PeriodType::class, $period);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            
+            $manager->persist($period);
+            $manager->flush();
+
+            return $this->redirectToRoute('app_admin_event_config_period', ['id' => $event->getId()]);
+        }
+
+        return $this->render('admin/event/config/period.html.twig', [
+            'event' => $event,
+            'form' => $form,
+        ]);
+    }
+
+    #[Route('/admin/period/{id}/delete', name: 'app_admin_period_delete', requirements: ['id' => '\d+'], methods: ['GET'])]
+    public function delete(?Period $period, EntityManagerInterface $manager): Response
+    {
+        if ($period->isLocked) {
+            $this->addFlash('danger', 'Cette période est utilisée dans les slots ! Supprimez les slots avant de supprimer la période !');
+        } else {
+            $event = $period->getEvent();
+            $manager->remove($period);
+            $manager->flush();
+        }
+        
+        return $this->redirectToRoute('app_admin_event_config_period', ['id' => $event->getId()]);
+    }
+}

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

@@ -0,0 +1,115 @@
+<?php
+
+namespace App\Controller\Admin\EventConfig;
+
+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\Event;
+use App\Entity\Slot;
+use App\Repository\SlotRepository;
+
+use App\Service\DateTimeHelper;
+
+final class SlotController extends AbstractController
+{
+    #[Route('/admin/event/{id}/configure/slot', name: 'app_admin_event_config_slot', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function index(?Event $event, Request $request, EntityManagerInterface $manager, SlotRepository $repository): Response
+    {
+        return $this->render('admin/event/config/slot.html.twig', [
+            'event' => $event,
+        ]);
+    }
+
+    #[Route('/admin/event/{id}/configure/slot/generate', name: 'app_main_event_config_slot_generate', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function generate(?Event $event, Request $request, EntityManagerInterface $manager, SlotRepository $repository, DateTimeHelper $dateTimeHelper): Response
+    {
+        // On réalise avant tous les contrôles, sinon, on alerte
+        if (count($event->getSpaces()) < 1)
+        {
+            // Aucun slot n'est généré, on quitte
+            $this->addFlash('danger', 'Aucun espace disponible !');
+        }
+        if (count($event->getPeriods()) < 1)
+        {
+            // Aucun slot n'est généré, on quitte
+            $this->addFlash('danger', 'Aucune période disponible !');
+        }
+        if (count($event->getSlots()) > 0)
+        {
+            // Des slots existent déjà, on quitte
+            $this->addFlash('danger', 'Supprimez les slots existants avant de régénérer !');
+        }
+        else
+        {
+            // C'est bon, on a des espaces, des périodes et pas encore de slots
+            // On incrémente sur les espaces
+            $countGenSlot = 0;
+            foreach ($event->getSpaces() as $space)
+            {
+                // Et ensuite sur les périodes
+                foreach ($event->getPeriods() as $period)
+                {
+                    // RAPPEL : l'id généré n'est pas une donnée FIABLE !
+                    // Composer les plannings avec startOn suivent == endOn précédent pour descendre les colonnes
+                    // et startOn courant == startOn courant avec un même "event" pour les lignes
+                    
+                    // StatIndex, on commence à la première heure entière ou demi-heure entière qui suit le début de la période
+                    $startIndex = $dateTimeHelper->roundUpToNextSlot($period->getStartOn());
+
+                    // Tant que $startIndex est < $period->getEndOn(), on s'arrête au dernier "bout de slot" avant la fin
+                    while ($startIndex < $period->getEndOn())
+                    {
+                        // On génère chaque slot de cette période pour cet espace et surtout, cet événement et on le marque comme disponible
+                        $newSlot = new Slot();
+                        $newSlot->setEvent($event);
+                        $newSlot->setSpace($space);
+                        $newSlot->setPeriod($period);
+                        $newSlot->setUnavailable(false);
+                        // Ajout de la date de début = Index
+                        $newSlot->setStartOn($startIndex);
+                        // on incrémente l'index de 30 minutes pour la suite
+                        $startIndex->add(new \DateInterval('PT30M'));
+                        // et on l'utilise aussi pour la fin
+                        $newSlot->setEndOn($startIndex);
+                        // Hop, dans la BDD en attente de sauvegarde
+                        $manager->persist($newSlot);
+                        $countGenSlot++;
+                    }
+                    // On marque la période comme utilisée et vérouillée
+                    $period->setLocked(true);
+                    $manager->persist($period);
+                }
+                // On marque l'espace comme utilisé et vérouillé
+                $space->setLocked(true);
+                $manager->persist($space);
+            }
+            $manager->flush();
+            $controlValue = count($event->getSlots());
+            $this->addFlash('info', $countGenSlot.' slots générés. '.$controlValue.' slots enregistés en base');
+        }
+
+        // On retourne à la page de configuration
+        return $this->redirectToRoute('app_admin_event_config_slot', ['id' => $event->getId()]);
+    }
+
+    #[Route('/admin/event/{id}/configure/slot/delete', name: 'app_main_event_config_slot_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function delete(?Event $event, Request $request, EntityManagerInterface $manager, SlotRepository $repository): Response
+    {
+        $slots = $event->getSlots();
+        foreach ($slots as $slot) {
+            $slot->getPeriod()->setLocked(false);
+            $slot->getSpace()->setLocked(false);
+            $manager->remove($slot);
+        }
+        $manager->flush();
+        // On retourne à la page de configuration
+        return $this->redirectToRoute('app_admin_event_config_slot', ['id' => $event->getId()]);        
+    }
+
+
+}

+ 55 - 0
src/Controller/Admin/EventConfig/SpaceController.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Controller\Admin\EventConfig;
+
+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\Event;
+use App\Entity\Space;
+use App\Form\SpaceType;
+use App\Repository\SpaceRepository;
+
+final class SpaceController extends AbstractController
+{
+    #[Route('/admin/event/{id}/configure/space', name: 'app_admin_event_config_space', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function index(?Event $event, Request $request, EntityManagerInterface $manager, SpaceRepository $repository): Response
+    {
+        $space = new Space();
+        $space->setEvent($event);
+        
+        $form = $this->createForm(SpaceType::class, $space);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            
+            $manager->persist($space);
+            $manager->flush();
+
+            return $this->redirectToRoute('app_admin_event_config_space', ['id' => $event->getId()]);
+        }
+
+        return $this->render('admin/event/config/space.html.twig', [
+            'event' => $event,
+            'form' => $form,
+        ]);
+    }
+
+    #[Route('/admin/space/{id}/delete', name: 'app_admin_space_delete', requirements: ['id' => '\d+'], methods: ['GET'])]
+    public function delete(?Space $space, EntityManagerInterface $manager): Response
+    {
+        if ($space->isLocked) {
+            $this->addFlash('danger', 'Cet espace est utilisé dans les slots ! Supprimez les slots avant de supprimer l\'espace !');
+        } else {
+            $event = $space->getEvent();
+            $manager->remove($space);
+            $manager->flush();
+        }
+
+        return $this->redirectToRoute('app_admin_event_config_space', ['id' => $event->getId()]);
+    }
+}

+ 158 - 0
src/Controller/Admin/EventController.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace App\Controller\Admin;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Requirement\Requirement;
+use Symfony\Component\Routing\Attribute\Route;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\String\Slugger\AsciiSlugger;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
+
+use App\Entity\Event;
+use App\Form\EventType;
+use App\Repository\EventRepository;
+use App\Service\PictureService;
+
+final class EventController extends AbstractController
+{
+    /*
+     * Lister tous les événements
+     */
+    #[Route('/admin/event', name: 'app_admin_event')]
+    public function index(EventRepository $repository): Response
+    {
+        $events = $repository->findAll();
+
+        return $this->render('admin/event/index.html.twig', [
+            'events' => $events,
+        ]);
+    }
+
+    /*
+     * Supprimer un événement
+     */
+    #[Route('/admin/event/{id}/delete', name: 'app_admin_event_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function delete(?Event $event, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        // Effacer toutes images associée
+        if ($event->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/events/' . $event->getPicture();
+            unlink($fullPath);
+        }
+        
+        $manager->remove($event);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Événement supprimé avec succès.');
+        return $this->redirectToRoute('app_admin_event');
+    }
+
+    /*
+     * Supprimer l'image d'un événement
+     */
+    #[Route('/admin/event/{id}/del-pic', name: 'app_admin_event_del_pic', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function deletePicture(?Event $event, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        // Effacer toutes images associée
+        if ($event->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/events/' . $event->getPicture();
+            unlink($fullPath);
+        }
+        $event->setPicture(null);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Illustration supprimée avec succès.');
+        return $this->redirectToRoute('app_admin_event_edit', ['id' => $event->getId()]);
+    }
+
+    /* 
+     * Modifier un jeu
+     */
+    #[Route('/admin/event/{id}/edit', name: 'app_admin_event_edit', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function edit(?Event $event, Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        $form = $this->createForm(EventType::class, $event);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            $slug = $form->get('slug')->getData();
+            // Aucun slug n'a été proposé par l'utilisateur
+            if (!$slug) {
+                $slugger = new AsciiSlugger('fr_FR');
+                $slug = $slugger->slug(strtolower($event->getName()));
+                $event->setSlug($slug);
+            }
+            
+            // Traiter l'image proposée
+            $tmpPicture = $form->get('picture')->getData();
+            if ($tmpPicture) {
+                $picture = $pictureService->banner($tmpPicture, '/events/', $slug);
+                $event->setPicture($picture);
+            }
+            $manager->persist($event);
+            $manager->flush();
+
+            $this->addFlash('success', 'Événement modifié avec succès.');
+            return $this->redirectToRoute('app_admin_event');
+        }
+
+        return $this->render('admin/event/edit.html.twig', [
+            'form' => $form,
+            'event' => $event,
+        ]);
+    }
+
+    /* 
+     * Ajouter un jeu
+     */
+    #[Route('/admin/event/add', name: 'app_admin_event_add', methods: ['GET', 'POST'])]
+    public function add(Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        // Initialisation d'un nouveau jeu
+        $event = new Event();
+
+        $form = $this->createForm(EventType::class, $event);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            
+            $slug = $form->get('slug')->getData();
+            // Aucun slug n'a été proposé par l'utilisateur
+            if (!$slug) {
+                $slugger = new AsciiSlugger('fr_FR');
+                $slug = $slugger->slug(strtolower($event->getName()));
+                $event->setSlug($slug);
+            }
+            
+            // Traiter l'image proposée
+            $tmpPicture = $form->get('picture')->getData();
+            if ($tmpPicture) {
+                $picture = $pictureService->banner($tmpPicture, '/events/', $slug);
+                $event->setPicture($picture);
+            }
+
+            $manager->persist($event);
+            $manager->flush();
+
+            $this->addFlash('success', 'Événement ajouté à la base de données avec succès.');
+            return $this->redirectToRoute('app_admin_event');
+        }
+
+        return $this->render('admin/event/edit.html.twig', [
+            'form' => $form,
+            'event' => $event
+        ]);
+    }
+
+    #[Route('/admin/event/{id}/configure', name: 'app_admin_event_config', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function configure(?Event $event, Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        return $this->redirectToRoute('app_admin_event_config_space', [
+            'id' => $event->getId()
+        ]);
+    }
+
+}

+ 0 - 3
src/Controller/Admin/UserController.php

@@ -204,7 +204,4 @@ final class UserController extends AbstractController
             'form' => $form,
         ]);
     }
-
-
-
 }

+ 135 - 18
src/Entity/Event.php

@@ -3,6 +3,8 @@
 namespace App\Entity;
 
 use App\Repository\EventRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\DBAL\Types\Types;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Bridge\Doctrine\Types\UuidType;
@@ -27,11 +29,11 @@ class Event
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $picture = null;
 
-    #[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
-    private ?\DateTimeImmutable $startOn = null;
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $startOn = null;
 
-    #[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
-    private ?\DateTimeImmutable $endOn = null;
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $endOn = null;
 
     #[ORM\Column(type: Types::ASCII_STRING, nullable: true)]
     private $moreLink;
@@ -43,14 +45,39 @@ class Event
     private ?bool $published = null;
 
     #[ORM\Column(nullable: true)]
-    private ?bool $public = null;
+    private ?bool $private = null;
 
     #[ORM\Column(nullable: true)]
-    private ?bool $everyoneCanShowPlanning = null;
+    private ?bool $hiddenPlanning = null;
 
     #[ORM\Column(nullable: true)]
     private ?bool $everyoneCanAskForGame = null;
 
+    /**
+     * @var Collection<int, Space>
+     */
+    #[ORM\OneToMany(targetEntity: Space::class, mappedBy: 'event', orphanRemoval: true)]
+    private Collection $spaces;
+
+    /**
+     * @var Collection<int, Period>
+     */
+    #[ORM\OneToMany(targetEntity: Period::class, mappedBy: 'event', orphanRemoval: true)]
+    private Collection $periods;
+
+    /**
+     * @var Collection<int, Slot>
+     */
+    #[ORM\OneToMany(targetEntity: Slot::class, mappedBy: 'event', orphanRemoval: true)]
+    private Collection $slots;
+
+    public function __construct()
+    {
+        $this->spaces = new ArrayCollection();
+        $this->periods = new ArrayCollection();
+        $this->slots = new ArrayCollection();
+    }
+
     public function getId(): ?Uuid
     {
         return $this->id;
@@ -92,24 +119,24 @@ class Event
         return $this;
     }
 
-    public function getStartOn(): ?\DateTimeImmutable
+    public function getStartOn(): ?\DateTimeInterface
     {
         return $this->startOn;
     }
 
-    public function setStartOn(\DateTimeImmutable $startOn): static
+    public function setStartOn(\DateTimeInterface $startOn): static
     {
         $this->startOn = $startOn;
 
         return $this;
     }
 
-    public function getEndOn(): ?\DateTimeImmutable
+    public function getEndOn(): ?\DateTimeInterface
     {
         return $this->endOn;
     }
 
-    public function setEndOn(\DateTimeImmutable $endOn): static
+    public function setEndOn(\DateTimeInterface $endOn): static
     {
         $this->endOn = $endOn;
 
@@ -152,26 +179,26 @@ class Event
         return $this;
     }
 
-    public function isPublic(): ?bool
+    public function isPrivate(): ?bool
     {
-        return $this->public;
+        return $this->private;
     }
 
-    public function setPublic(?bool $public): static
+    public function setPrivate(?bool $private): static
     {
-        $this->public = $public;
+        $this->private = $private;
 
         return $this;
     }
 
-    public function isEveryoneCanShowPlanning(): ?bool
+    public function isHiddenPlanning(): ?bool
     {
-        return $this->everyoneCanShowPlanning;
+        return $this->hiddenPlanning;
     }
 
-    public function setEveryoneCanShowPlanning(?bool $everyoneCanShowPlanning): static
+    public function setHiddenPlanning(?bool $hiddenPlanning): static
     {
-        $this->everyoneCanShowPlanning = $everyoneCanShowPlanning;
+        $this->hiddenPlanning = $hiddenPlanning;
 
         return $this;
     }
@@ -187,4 +214,94 @@ class Event
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Space>
+     */
+    public function getSpaces(): Collection
+    {
+        return $this->spaces;
+    }
+
+    public function addSpace(Space $space): static
+    {
+        if (!$this->spaces->contains($space)) {
+            $this->spaces->add($space);
+            $space->setEvent($this);
+        }
+
+        return $this;
+    }
+
+    public function removeSpace(Space $space): static
+    {
+        if ($this->spaces->removeElement($space)) {
+            // set the owning side to null (unless already changed)
+            if ($space->getEvent() === $this) {
+                $space->setEvent(null);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, Period>
+     */
+    public function getPeriods(): Collection
+    {
+        return $this->periods;
+    }
+
+    public function addPeriod(Period $period): static
+    {
+        if (!$this->periods->contains($period)) {
+            $this->periods->add($period);
+            $period->setEvent($this);
+        }
+
+        return $this;
+    }
+
+    public function removePeriod(Period $period): static
+    {
+        if ($this->periods->removeElement($period)) {
+            // set the owning side to null (unless already changed)
+            if ($period->getEvent() === $this) {
+                $period->setEvent(null);
+            }
+        }
+
+        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->setEvent($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->getEvent() === $this) {
+                $slot->setEvent(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 81 - 0
src/Entity/Period.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\PeriodRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity(repositoryClass: PeriodRepository::class)]
+class Period
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\Column]
+    private ?\DateTime $startOn = null;
+
+    #[ORM\Column]
+    private ?\DateTime $endOn = null;
+
+    #[ORM\ManyToOne(inversedBy: 'periods')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Event $event = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $locked = null;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    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 getEvent(): ?Event
+    {
+        return $this->event;
+    }
+
+    public function setEvent(?Event $event): static
+    {
+        $this->event = $event;
+
+        return $this;
+    }
+
+    public function isLocked(): ?bool
+    {
+        return $this->locked;
+    }
+
+    public function setLocked(?bool $locked): static
+    {
+        $this->locked = $locked;
+
+        return $this;
+    }
+}

+ 113 - 0
src/Entity/Slot.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\SlotRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity(repositoryClass: SlotRepository::class)]
+class Slot
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Period $period = null;
+
+    #[ORM\ManyToOne]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Space $space = null;
+
+    #[ORM\ManyToOne(inversedBy: 'slots')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Event $event = null;
+
+    #[ORM\Column]
+    private ?\DateTime $startOn = null;
+
+    #[ORM\Column]
+    private ?\DateTime $endOn = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $unavailable = null;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getPeriod(): ?Period
+    {
+        return $this->period;
+    }
+
+    public function setPeriod(?Period $period): static
+    {
+        $this->period = $period;
+
+        return $this;
+    }
+
+    public function getSpace(): ?Space
+    {
+        return $this->space;
+    }
+
+    public function setSpace(?Space $space): static
+    {
+        $this->space = $space;
+
+        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 isUnavailable(): ?bool
+    {
+        return $this->unavailable;
+    }
+
+    public function setUnavailable(?bool $unavailable): static
+    {
+        $this->unavailable = $unavailable;
+
+        return $this;
+    }
+}

+ 66 - 0
src/Entity/Space.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\SpaceRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity(repositoryClass: SpaceRepository::class)]
+class Space
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\ManyToOne(inversedBy: 'spaces')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Event $event = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $locked = null;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getName(): ?string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): static
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    public function getEvent(): ?Event
+    {
+        return $this->event;
+    }
+
+    public function setEvent(?Event $event): static
+    {
+        $this->event = $event;
+
+        return $this;
+    }
+
+    public function isLocked(): ?bool
+    {
+        return $this->locked;
+    }
+
+    public function setLocked(?bool $locked): static
+    {
+        $this->locked = $locked;
+
+        return $this;
+    }
+}

+ 24 - 0
src/Form/ContactType.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class ContactType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('field_name')
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            // Configure your form options here
+        ]);
+    }
+}

+ 122 - 0
src/Form/EventType.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Event;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\FileType;
+use Symfony\Component\Form\Extension\Core\Type\UrlType;
+use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
+
+class EventType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('name', null, [
+                'label' => 'Nom de l\'événement',
+                'required' => true,
+                'help' => 'Entrez le nom de l\'événement',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],                
+            ])
+            ->add('startOn', DateTimeType::class, [
+                'widget' => 'single_text',
+                'help' => 'La date est enregistrée pour le fuseau horaire : '.$_ENV['APP_TZ'],
+                'label' => 'Date et heure de début',
+                'view_timezone' => $_ENV['APP_TZ'],
+                'html5' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],   
+            ])
+            ->add('endOn', DateTimeType::class, [
+                'widget' => 'single_text',
+                'help' => 'La date est enregistrée pour le fuseau horaire : '.$_ENV['APP_TZ'],
+                'label' => 'Date et heure de fin',
+                'view_timezone' => $_ENV['APP_TZ'],
+                'html5' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],   
+            ])
+            ->add('description', null, [
+                'label' => 'Description',
+                'label_attr' => ['class' => 'label'],
+                'help' => 'Description de l\'événement.',
+                'attr' => ['class' => 'textarea',
+                           'rows' => 6],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+
+            ])
+            ->add('picture', FileType::class, [
+                'label' => 'Illustration',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'file-input'],
+                'help' => 'Fichier JPEG, PNG ou webP, bannière horizontale, sera découpée et redimensionnée en 600×200',
+                'required' => false,
+                'mapped' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+
+            ->add('moreLink', UrlType::class, [
+                'label' => 'Lien vers un complément d\'information',
+                'help' => 'Lien vers une page décrivant l\'événement.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('slug', null, [
+                'label' => 'Slug',
+                'help' => 'Laissez vide pour le générer automatiquement, utilisé dans les URL.',
+                'required' => false,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('private', null, [
+                'label' => 'Événement privé',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si cet événement n\'est visible que des utilisateur(rice)s ayant au moins le rôle "Équipe".',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('hiddenPlanning', null, [
+                'label' => 'Planning caché',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si le planning des parties n\'est visible que des utilisateur(rice)s ayant au moins le rôle "Gestionnaire".',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('everyoneCanAskForGame', null, [
+                'label' => 'Propositions de parties',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si les participant(e)s peuvent demander l\'organisation de parties.',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Event::class,
+        ]);
+    }
+}

+ 49 - 0
src/Form/PeriodType.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Event;
+use App\Entity\Period;
+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 PeriodType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('startOn', DateTimeType::class, [
+                'widget' => 'single_text',
+                'help' => 'La date est enregistrée pour le fuseau horaire : '.$_ENV['APP_TZ'],
+                'label' => 'Date et heure de début',
+                'view_timezone' => $_ENV['APP_TZ'],
+                'html5' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],   
+            ])
+            ->add('endOn', DateTimeType::class, [
+                'widget' => 'single_text',
+                'help' => 'La date est enregistrée pour le fuseau horaire : '.$_ENV['APP_TZ'],
+                'label' => 'Date et heure de fin',
+                'view_timezone' => $_ENV['APP_TZ'],
+                'html5' => 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' => Period::class,
+        ]);
+    }
+}

+ 35 - 0
src/Form/SpaceType.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Event;
+use App\Entity\Space;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class SpaceType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('name', null, [
+                'label' => 'Nom de l\'espace',
+                'required' => true,
+                'help' => 'Entrez un nom pour cet espace.',
+                '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' => Space::class,
+        ]);
+    }
+}

+ 43 - 0
src/Repository/PeriodRepository.php

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

+ 72 - 0
src/Repository/SlotRepository.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Slot;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Slot>
+ */
+class SlotRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Slot::class);
+    }
+
+    public function findNext($slot): ?Slot
+    {
+        $qb = $this->createQueryBuilder('s')
+            ->where('s.startOn = :endThis')
+            ->andWhere('s.period = :periodThis')
+            ->andWhere('s.space = :spaceThis')
+            ->setParameter('endThis', $slot->getEndOn())
+            ->setParameter('periodThis', $slot->getPeriod())
+            ->setParameter('spaceThis', $slot->getSpace())
+            ->getQuery();
+        
+        return $qb->getOneOrNullResult();
+    }
+
+    public function findPrevious($slot): ?Slot
+    {
+        $qb = $this->createQueryBuilder('s')
+            ->where('s.endOn = :startThis')
+            ->andWhere('s.period = :periodThis')
+            ->andWhere('s.space = :spaceThis')
+            ->setParameter('startThis', $slot->getStartOn())
+            ->setParameter('periodThis', $slot->getPeriod())
+            ->setParameter('spaceThis', $slot->getSpace())
+            ->getQuery();
+        
+        return $qb->getOneOrNullResult();
+    }
+
+
+    //    /**
+    //     * @return Slot[] Returns an array of Slot objects
+    //     */
+    //    public function findByExampleField($value): array
+    //    {
+    //        return $this->createQueryBuilder('s')
+    //            ->andWhere('s.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->orderBy('s.id', 'ASC')
+    //            ->setMaxResults(10)
+    //            ->getQuery()
+    //            ->getResult()
+    //        ;
+    //    }
+
+    //    public function findOneBySomeField($value): ?Slot
+    //    {
+    //        return $this->createQueryBuilder('s')
+    //            ->andWhere('s.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->getQuery()
+    //            ->getOneOrNullResult()
+    //        ;
+    //    }
+}

+ 43 - 0
src/Repository/SpaceRepository.php

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

+ 26 - 0
src/Service/DateTimeHelper.php

@@ -0,0 +1,26 @@
+<?php
+
+// src/Service/SlotTimeHelper.php
+namespace App\Service;
+
+class DateTimeHelper
+{
+    public function roundUpToNextSlot(\DateTimeInterface $dateTime): \DateTime
+    {
+        $minutes = (int) $dateTime->format('i');
+        $rounded = new \DateTime($dateTime->format('Y-m-d H:i:s'), $dateTime->getTimezone());
+
+        if ($minutes === 0 || $minutes === 30) {
+            $rounded->setTime((int)$rounded->format('H'), $minutes, 0);
+            return $rounded;
+        }
+
+        if ($minutes < 30) {
+            $rounded->setTime((int)$rounded->format('H'), 30, 0);
+        } else {
+            $rounded->modify('+1 hour')->setTime((int)$rounded->format('H'), 0, 0);
+        }
+
+        return $rounded;
+    }
+}

+ 103 - 0
templates/admin/event/config/period.html.twig

@@ -0,0 +1,103 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Événement > Configurer{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li><a href="{{ path('app_admin') }}">Administration</a></li>
+        <li><a href="{{ path('app_admin_event') }}">Gestion des événements</a></li>
+        <li class="is-active"><a>Configuration</a></li>
+      </ul>
+    </nav>
+
+    <div class="box has-text-centered">
+      <strong class="is-primary">{{ event.name }}</strong> du {{ event.startOn|date('d/m/Y H:i', app_timezone) }} au {{ event.endOn|date('d/m/Y H:i', app_timezone) }}
+    </div>
+    <!--<div class="block">
+      <p>Déterminez des <strong>espaces</strong> et des <strong>périodes</strong> pour générer les <strong>slots</strong> permettant de modéliser le planning.
+      Les <strong>slots</strong> peuvent être régénérés à tout moment, mais peuvent entraîner la destruction de parties déjà enregistrées pour l'événement.
+      Les <strong>jeux</strong> sont sélectionnés à partir des <strong>MJ</strong> affectés à l'événement. Seul les <strong>jeux</strong> et <strong>MJ</strong> sélectionnés pourront être proposés pour créer le planning.
+      Les <strong>MJ</strong> disposant d'un compte pourront ajouter eux-mêmes leurs parties et définir leurs disponibilités.
+      Les <strong>périodes off</strong> permettent de neutraliser des <strong>slots</strong> dans le planning où aucune partie ne pourra être enregistrer dans l'<strong>espace spécifié</strong>.</p>
+    </div>-->
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li><a href="{{ path('app_admin_event_config_space', {'id': event.id}) }}">Espaces</a></li>
+            <li class="is-active"><a>Périodes</a></li>
+            <li><a href="{{ path('app_admin_event_config_slot', {'id': event.id}) }}">Slots</a></li>
+            <li><a>Meneur(euse)s de jeu</a></li>
+            <li><a>Jeux</a></li>
+        </ul>
+    </div>
+
+    <div id="tabs-content">
+
+    <div class="container">
+      
+
+    
+    <div class="columns">
+    <div class="column">
+      
+
+            <div class="block">
+      <h3 class="title is-3">Configuration des périodes</h3>
+      <p>Les <strong>périodes</strong> représentent les lieux plages horaires lors desquelles des animations seront proposées.</p>
+      </div>
+      <div class="box">
+      {{ form_errors(form) }}
+      {{ form_start(form) }}
+        <div class="columns">
+          <div class="column">
+            {{ form_row(form.startOn) }}
+          </div>
+          <div class="column">
+            {{ form_row(form.endOn) }}
+          </div>
+        </div>
+      {{ form_widget(form) }}
+      <div class="control"><button class="button is-primary" type="submit">Ajouter</button></div>
+      {{ form_end(form) }}
+    </div>
+    </div>
+
+    <div class="column">
+        <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
+            <thead>
+            <tr>
+                <th>Début</th>
+                <th>Fin</th>
+                <th>Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for period in event.getPeriods %}
+                <tr>
+                    <td>{{ period.startOn|date('d/m/Y H:i', app_timezone) }}</td>
+                    <td>{{ period.endOn|date('d/m/Y H:i', app_timezone) }}</td>
+                    <td>
+                      {% if period.isLocked %}
+                        <small class="id-danger">Utilisé dans le calcul des slots.</small>
+                      {% else %}
+                        <a class="button is-danger" data-id="{{ path('app_admin_period_delete', {'id': period.id})}}" href="#" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>
+                      {% endif %}
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+    </div>
+
+    
+      
+    </div>
+    </div>
+
+    </div>
+
+{% endblock %}

+ 104 - 0
templates/admin/event/config/slot.html.twig

@@ -0,0 +1,104 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Événement > Configurer{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li><a href="{{ path('app_admin') }}">Administration</a></li>
+        <li><a href="{{ path('app_admin_event') }}">Gestion des événements</a></li>
+        <li class="is-active"><a>Configuration</a></li>
+      </ul>
+    </nav>
+
+    <div class="box has-text-centered">
+      <strong class="is-primary">{{ event.name }}</strong> du {{ event.startOn|date('d/m/Y H:i', app_timezone) }} au {{ event.endOn|date('d/m/Y H:i', app_timezone) }}
+    </div>
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li><a href="{{ path('app_admin_event_config_space', {'id': event.id}) }}">Espaces</a></li>
+            <li><a href="{{ path('app_admin_event_config_period', {'id': event.id}) }}">Périodes</a></li>
+            <li class="is-active"><a>Slots</a></li>
+            <li><a>Meneur(euse)s de jeu</a></li>
+            <li><a>Jeux</a></li>
+        </ul>
+    </div>
+
+    <div id="tabs-content">
+      <div class="container">
+        <div class="columns">
+          <div class="column">
+            <div class="block">
+              <h3 class="title is-3">Configuration des slots</h3>
+              <p>Les <strong>slot</strong> représentent des unités de temps de 30 minutes qui vont accueillir les animations afin de calculer simplement les disponibilités des espaces et/ou des MJ. Ils sont générés à partir des espaces et des périodes. Si vous modifiez, ajoutez ou supprimez des espaces ou des périodes après génération des slots, vous devrez les supprimer, puis relancer leur génération ensuite.</p>
+            </div>
+            <div class="box">
+              <div class="columns">
+                <div class="column">
+                  {% if event.getSpaces()|length > 0 %}
+                    {{ event.getSpaces()|length }} espaces
+                  {% else %}
+                    Aucun espace
+                  {% endif %}
+                </div>
+                <div class="column">
+                  {% if event.getPeriods()|length > 0 %}
+                    {{ event.getPeriods()|length }} périodes
+                  {% else %}
+                    Aucune période
+                  {% endif %}
+                </div>
+                <div class="column">
+                  {% if event.getSlots()|length > 0 %}
+                    {{ event.getSlots()|length }} slots
+                  {% else %}
+                    Aucune slot
+                  {% endif %}
+                </div>
+                <div class="column">
+                  {% if event.getPeriods()|length > 0 and  event.getSpaces()|length > 0 and event.getSlots()|length < 1 %}
+                    <a href="#" data-id="{{ path('app_main_event_config_slot_generate', {id: event.getId })}}" class="button is-primary"  {{ stimulus_controller('admin_confirm') }}>Générer les slots</a>
+                  {% elseif event.getSlots()|length > 0 %}
+                    <a href="#" data-id="{{ path('app_main_event_config_slot_delete', {id: event.getId })}}" class="button is-danger" {{ stimulus_controller('admin_confirm') }}>Supprimer les slots</a>
+                  {% else %}
+                    <button href="#" class="button" disabled>Générer les slots</button>
+                  {% endif %}
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="column">
+            <div class="block">
+              <h3 class="title is-3">Aperçu du planning</h3>
+              <p>Cliquez sur les <strong>slots</strong> qui sont indisponibles.</p>
+            </div>
+            {% if event.getSlots()|length > 0 %}
+              CA ARRRIVE
+            {% else %}
+              <article class="message is-danger">
+                  <div class="message-header">
+                    <p>Aucun slot généré</p>
+                    <button class="delete" aria-label="delete"></button>
+                  </div>
+                  <div class="message-body">
+                      Le planning n'est pas encore consultable. Générez les slots à l'aide du bouton à gauche de l'écran.
+                  </div>
+              </article>
+            {% endif %}
+          </div>
+
+
+
+
+        </div>
+
+
+
+      </div>
+    </div>
+
+
+{% endblock %}

+ 94 - 0
templates/admin/event/config/space.html.twig

@@ -0,0 +1,94 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Événement > Configurer{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li><a href="{{ path('app_admin') }}">Administration</a></li>
+        <li><a href="{{ path('app_admin_event') }}">Gestion des événements</a></li>
+        <li class="is-active"><a>Configuration</a></li>
+      </ul>
+    </nav>
+
+    <div class="box has-text-centered">
+      <strong class="is-primary">{{ event.name }}</strong> du {{ event.startOn|date('d/m/Y H:i', app_timezone) }} au {{ event.endOn|date('d/m/Y H:i', app_timezone) }}
+    </div>
+    <!--<div class="block">
+      <p>Déterminez des <strong>espaces</strong> et des <strong>périodes</strong> pour générer les <strong>slots</strong> permettant de modéliser le planning.
+      Les <strong>slots</strong> peuvent être régénérés à tout moment, mais peuvent entraîner la destruction de parties déjà enregistrées pour l'événement.
+      Les <strong>jeux</strong> sont sélectionnés à partir des <strong>MJ</strong> affectés à l'événement. Seul les <strong>jeux</strong> et <strong>MJ</strong> sélectionnés pourront être proposés pour créer le planning.
+      Les <strong>MJ</strong> disposant d'un compte pourront ajouter eux-mêmes leurs parties et définir leurs disponibilités.
+      Les <strong>périodes off</strong> permettent de neutraliser des <strong>slots</strong> dans le planning où aucune partie ne pourra être enregistrer dans l'<strong>espace spécifié</strong>.</p>
+    </div>-->
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li class="is-active"><a>Espaces</a></li>
+            <li><a href="{{ path('app_admin_event_config_period', {'id': event.id}) }}">Périodes</a></li>
+            <li><a href="{{ path('app_admin_event_config_slot', {'id': event.id}) }}">Slots</a></li>
+            <li><a>Meneur(euse)s de jeu</a></li>
+            <li><a>Jeux</a></li>
+        </ul>
+    </div>
+
+    <div id="tabs-content">
+
+    <div class="container">
+      
+
+    
+    <div class="columns">
+    <div class="column">
+      
+
+            <div class="block">
+      <h3 class="title is-3">Configuration des espaces</h3>
+      <p>Les <strong>espaces</strong> représentent les lieux disponibles pour les animations prévues : table, salle... Un espace peut accueillir une partie à la fois sur un horaire donné.</p>
+      </div>
+      <div class="box">
+      {{ form_errors(form) }}
+      {{ form_start(form) }}
+
+      {{ form_widget(form) }}
+      <div class="control"><button class="button is-primary" type="submit">Ajouter</button></div>
+      {{ form_end(form) }}
+    </div>
+    </div>
+
+    <div class="column">
+        <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
+            <thead>
+            <tr>
+                <th>Nom</th>
+                <th>Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for space in event.getSpaces %}
+                <tr>
+                    <td>{{ space.name }}</td>
+                    <td>
+                      {% if space.isLocked %}
+                        <small class="id-danger">Utilisé dans le calcul des slots.</small>
+                      {% else %}
+                        <a class="button is-danger" data-id="{{ path('app_admin_space_delete', {'id': space.id})}}" href="#" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>
+                      {% endif %}
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+    </div>
+
+    
+      
+    </div>
+    </div>
+
+    </div>
+
+{% endblock %}

+ 93 - 0
templates/admin/event/edit.html.twig

@@ -0,0 +1,93 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Événement > Éditer{% endblock %}
+
+{% block content %}
+
+    {{ form_errors(form) }}
+    {{ form_start(form) }}
+
+    {{ form_row(form.name) }}
+    <div class="box">
+        <div class="columns">
+            <div class="column">{{ form_row(form.startOn) }}</div>
+            <div class="column">{{ form_row(form.endOn) }}</div>
+        </div>
+        <div class="columns">
+            <div class="column">
+                <div class="field">
+                    {{ form_widget(form.private) }}
+                    {{ form_label(form.private) }}
+                    {{ form_help(form.private) }}
+                </div>
+            </div>
+            <div class="column">
+                <div class="field">
+                    {{ form_widget(form.hiddenPlanning) }}
+                    {{ form_label(form.hiddenPlanning) }}
+                    {{ form_help(form.hiddenPlanning) }}
+                </div>
+            </div>
+            <div class="column">
+                <div class="field">
+                    {{ form_widget(form.everyoneCanAskForGame) }}
+                    {{ form_label(form.everyoneCanAskForGame) }}
+                    {{ form_help(form.everyoneCanAskForGame) }}
+                </div>
+            </div>
+        </div>
+    </div>
+    {{ form_row(form.slug) }}
+    {{ form_row(form.description)}}
+
+    {# gestion de l'illustration #}
+    <div class="box">
+    <div class="field">
+        <div class="columns">
+            <div class="column">
+                {{ form_label(form.picture) }}
+                <div class="file has-name is-fullwidth" id="file-js"  {{ stimulus_controller('bulma-filenames') }}>
+                <label class="file-label" >
+                    {{ form_widget(form.picture) }}
+                    <span class="file-cta">
+                    <span class="file-icon">
+                        <twig:ux:icon name="bi:cloud-upload" />
+                    </span>
+                    <span class="file-label"> Choisissez un fichier… </span>
+                    </span>
+                    <span class="file-name"> aucun fichier </span>
+                </label>
+                </div>
+                {{ form_help(form.picture) }}
+                {% if event.picture %}
+                <div class="field mt-2">
+                <p><a href="{{ path('app_admin_event_del_pic', {id: event.id}) }}" class="button is-danger"><twig:ux:icon name="bi:trash-fill" class="small-icon-in-text"/> Supprimer l'image chargée.</a></p>
+                </div>
+                {% endif %}
+                
+
+            </div>
+            <div class="column">
+                {% if event.picture %}
+                <img src="/images/events/{{ event.picture }}" class="image is-3by1"/>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+    </div>
+
+                {{ form_widget(form) }}
+
+
+
+
+
+
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+        <a href="{{ path('app_admin_event') }}" class="button">Annuler</a>
+    </div>
+
+    {{ form_end(form) }}
+
+{% endblock %}

+ 52 - 0
templates/admin/event/index.html.twig

@@ -0,0 +1,52 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Games{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li><a href="{{ path('app_admin') }}">Administration</a></li>
+        <li class="is-active"><a href="{{ path('app_admin_event') }}">Gestion des événements</a></li>
+      </ul>
+    </nav>
+
+    <div class="block">
+        <h1 class="title">Gestion des événement</h1>
+        <p class="subtitle">Liste des événements.</p>
+    </div>
+
+    <div class="block">
+        <div class="is-grouped">
+            <a class="button is-primary" href="{{ path('app_admin_event_add') }}">Ajouter un événement</a>
+        </div>
+    </div>
+
+        <div class="block">
+        <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
+            <thead>
+            <tr>
+                <th>Nom</th>
+                <th>Dates</th>
+                <th>Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for event in events %}
+                <tr>
+                    <td><a href="{{ path('app_admin_event_edit', {id: event.id}) }}">{{ event.name }}</a></td>
+                    <td>du {{ event.startOn|date('d/m/Y H:i', app_timezone) }}<br/>au {{ event.endOn|date('d/m/Y H:i', app_timezone) }}</td>
+                    <td>
+                        <a class="button" href="{{ path('app_admin_event_edit', {id: event.id}) }}">Éditer</a>
+                        <a class="button is-primary" href="{{ path('app_admin_event_config_space', {id: event.id}) }}">Configurer</a>
+                        <a class="button is-danger" data-id="{{ path('app_admin_event_delete', {'id': event.id})}}" href="#" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+    </div>
+
+{% endblock %}

+ 2 - 2
templates/admin/game/index.html.twig

@@ -1,6 +1,6 @@
 {% extends 'bulma.html.twig' %}
 
-{% block title %}Administration > Games{% endblock %}
+{% block title %}Administration > Jeux{% endblock %}
 
 {% block content %}
 
@@ -14,7 +14,7 @@
 
     <div class="block">
         <h1 class="title">Gestion des jeux</h1>
-        <p class="subtitle">Liste de jeux de rôle connus dans l'application.</p>
+        <p class="subtitle">Liste des jeux de rôle connus dans l'application.</p>
     </div>
 
     <div class="block">

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

@@ -36,7 +36,7 @@
           <div class="box">
             <h2 class="title is-4">Événements</h2>
             <p>Gérer les événements, plannifier et organiser.</p>
-            <p><a href="#">Gérer les événements</a></p>
+            <p><a href="{{ path('app_admin_event') }}">Gérer les événements</a></p>
           </div>
         </div>
         <!-- Colonne 3 -->

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 1
templates/bulma.html.twig


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott