diff --git a/assets/js/add-lieu.js b/assets/js/add-lieu.js index 063d2dd..d0a6a9a 100644 --- a/assets/js/add-lieu.js +++ b/assets/js/add-lieu.js @@ -1,23 +1,30 @@ document.addEventListener("DOMContentLoaded", () => { - console.log("Script chargé"); - - const addLieuButton = document.getElementById("add-lieu-button"); const addLieuModal = document.getElementById("add-lieu-modal"); const cancelAddLieu = document.getElementById("cancel-add-lieu"); - const selectLocationButton = document.getElementById("select-location"); - const addressDisplay = document.getElementById("lieu-details"); + const saveLieuButton = document.getElementById("save-lieu"); + const lieuNomInput = document.getElementById("lieu-nom"); + const lieuNomError = document.getElementById("lieu-nom-error"); + const villeSelect = document.getElementById("sortie_ville"); + const lieuSelect = document.getElementById("sortie_lieu"); - let map, marker, selectedAddress; + let map, marker, selectedAddress, selectedRue, cityPolygon, cityBounds; - if (addLieuButton && addLieuModal && cancelAddLieu && selectLocationButton) { - // Affiche la modal - addLieuButton.addEventListener("click", () => { - const villeId = document.getElementById("sortie_ville").value; + // Ouvrir la modal + document.getElementById("add-lieu-button").addEventListener("click", async () => { + const villeId = villeSelect.value; - if (!villeId) { - alert("Veuillez sélectionner une ville avant d'ajouter un lieu."); - return; + if (!villeId) { + alert("Veuillez sélectionner une ville avant d'ajouter un lieu."); + return; + } + + try { + // Récupérer les limites et le centre de la ville depuis le serveur + const response = await fetch(`/get-bounds/${villeId}`); + if (!response.ok) { + throw new Error("Erreur lors de la récupération des informations de la ville."); } + const data = await response.json(); addLieuModal.classList.remove("hidden"); @@ -28,155 +35,135 @@ document.addEventListener("DOMContentLoaded", () => { marker = null; } - // Récupère les limites de la ville - fetch(`/get-bounds/${villeId}`) - .then(response => { - if (!response.ok) { - throw new Error("Erreur lors de la récupération des limites de la ville"); - } - return response.json(); - }) - .then(data => { - console.log("Bounds de la ville :", data); + // Initialiser la carte + map = L.map("map").setView([data.centerLat, data.centerLng], 13); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + minZoom: 13, + }).addTo(map); - // Initialise la carte - map = L.map("map").setView([data.centerLat, data.centerLng], 13); + cityBounds = L.latLngBounds( + [data.south, data.west], + [data.north, data.east] + ); - // Ajout du fond de carte - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - maxZoom: 19, - }).addTo(map); + map.fitBounds(cityBounds); + map.setMaxBounds(cityBounds); - // Limite le déplacement de la carte aux bounds - const bounds = L.latLngBounds( - [data.south, data.west], - [data.north, data.east] - ); - map.setMaxBounds(bounds); - map.fitBounds(bounds); - - // Empêche de dézoomer au-delà de la ville - map.setMinZoom(map.getZoom()); - - // Gestion de la sélection d'un lieu sur la carte - map.on("click", async (e) => { - const { lat, lng } = e.latlng; - - // Place un marqueur - if (marker) { - marker.setLatLng([lat, lng]); - } else { - marker = L.marker([lat, lng]).addTo(map); - } - - // Récupère l'adresse à partir des coordonnées - try { - const response = await fetch( - `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}` - ); - if (!response.ok) { - throw new Error("Erreur lors de la récupération des informations du lieu."); - } - - const data = await response.json(); - console.log("Détails du lieu :", data); - - selectedAddress = data.display_name; - - // Affiche les détails du lieu - const addressDetails = ` -

Rue : ${data.address.road || "Non disponible"}

-

Ville : ${data.address.city || data.address.town || "Non disponible"}

-

Code postal : ${data.address.postcode || "Non disponible"}

-

Pays : ${data.address.country || "Non disponible"}

- `; - addressDisplay.innerHTML = addressDetails; - - } catch (error) { - console.error("Erreur lors de la récupération des informations :", error); - addressDisplay.textContent = "Impossible de récupérer l'adresse."; - } - }); - }) - .catch(error => { - console.error("Erreur :", error); - }); - }); - - // Ferme la modal sans enregistrer - cancelAddLieu.addEventListener("click", () => { - addLieuModal.classList.add("hidden"); - addressDisplay.innerHTML = ""; // Réinitialise les détails de l'adresse - }); - - // Sélectionne l'emplacement et envoie les données au serveur - selectLocationButton.addEventListener("click", () => { - if (marker) { - const lat = marker.getLatLng().lat; - const lng = marker.getLatLng().lng; - const villeId = document.getElementById("sortie_ville").value; - - if (!villeId) { - alert("Veuillez sélectionner une ville avant d'ajouter un lieu."); - return; - } - - if (!selectedAddress) { - alert("Veuillez sélectionner un emplacement sur la carte."); - return; - } - - // Demande le nom du lieu à l'utilisateur - const nom = prompt("Nom du lieu ?"); - if (!nom) { - alert("Le nom du lieu est obligatoire."); - return; - } - - // Envoie les données au serveur - fetch("/lieu/set", { - method: "POST", - headers: { - "Content-Type": "application/json", + // Ajouter le polygone de la ville si disponible + if (data.polygon_geojson) { + cityPolygon = L.geoJSON(data.polygon_geojson, { + style: { + color: "blue", + weight: 2, + fillOpacity: 0.3, }, - body: JSON.stringify({ - nom, - rue: selectedAddress, - latitude: lat, - longitude: lng, - villeId, - }), - }) - .then((response) => { - if (!response.ok) { - throw new Error("Erreur lors de l'ajout du lieu."); - } - return response.json(); - }) - .then((data) => { - console.log("Lieu ajouté :", data); - - // Ajoute le lieu à la liste déroulante - const lieuSelect = document.getElementById("sortie_lieu"); - const option = document.createElement("option"); - option.value = data.id; - option.textContent = data.nom; - lieuSelect.appendChild(option); - - // Sélectionne automatiquement le nouveau lieu - lieuSelect.value = data.id; - - // Ferme la modal - addLieuModal.classList.add("hidden"); - }) - .catch((error) => { - console.error("Erreur :", error); - }); - } else { - alert("Veuillez sélectionner un emplacement sur la carte."); + }).addTo(map); } - }); - } else { - console.error("Éléments requis pour ajouter un lieu introuvables."); - } + + // Gérer les clics sur la carte + map.on("click", async (e) => { + const { lat, lng } = e.latlng; + + if (!cityBounds.contains([lat, lng])) { + alert("Vous ne pouvez pas sélectionner un emplacement en dehors de la ville."); + return; + } + + selectedAddress = { lat, lng }; + + if (marker) { + marker.setLatLng([lat, lng]); + } else { + marker = L.marker([lat, lng]).addTo(map); + } + + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}` + ); + if (!response.ok) { + throw new Error("Erreur lors de la récupération des informations de l'adresse."); + } + + const data = await response.json(); + selectedRue = data.address.road || "Rue inconnue"; + } catch (error) { + console.error("Erreur lors de la recherche inversée :", error); + selectedRue = null; + alert("Impossible de récupérer l'adresse à partir de ces coordonnées."); + } + }); + } catch (error) { + console.error("Erreur :", error); + alert("Impossible de récupérer les données de la ville."); + } + }); + + // Fermer la modal + cancelAddLieu.addEventListener("click", () => { + addLieuModal.classList.add("hidden"); + lieuNomInput.value = ""; + lieuNomError.textContent = ""; + lieuNomError.classList.add("hidden"); + }); + + // Enregistrer le lieu + saveLieuButton.addEventListener("click", () => { + const nom = lieuNomInput.value.trim(); + + if (!nom) { + lieuNomError.textContent = "Le nom est obligatoire."; + lieuNomError.classList.remove("hidden"); + return; + } + + if (!selectedAddress || !selectedRue) { + alert("Veuillez sélectionner un emplacement valide sur la carte."); + return; + } + + const villeId = villeSelect.value; + if (!villeId) { + alert("Veuillez sélectionner une ville."); + return; + } + + // Réinitialiser les erreurs + lieuNomError.textContent = ""; + lieuNomError.classList.add("hidden"); + + // Envoyer les données au serveur + fetch("/lieu/set", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nom, + rue: selectedRue, + latitude: selectedAddress.lat, + longitude: selectedAddress.lng, + villeId, + }), + }) + .then((response) => response.json().then((data) => ({ status: response.status, data }))) + .then(({ status, data }) => { + if (status >= 200 && status < 300) { + // Rafraîchir la page après l'ajout réussi + location.reload(); + } else { + if (data.error.includes("nom")) { + lieuNomError.textContent = data.error; + lieuNomError.classList.remove("hidden"); + } else { + alert(data.error || "Une erreur est survenue."); + } + } + }) + .catch((error) => { + console.error("Erreur :", error); + alert("Une erreur inattendue s'est produite."); + }); + }); }); diff --git a/assets/js/lieu.js b/assets/js/lieu.js index 0f518cf..70d6729 100644 --- a/assets/js/lieu.js +++ b/assets/js/lieu.js @@ -34,6 +34,14 @@ document.addEventListener('DOMContentLoaded', function () { const option = document.createElement('option'); option.value = lieu.id; // Utiliser l'ID pour la soumission option.textContent = lieu.nom; // Texte visible dans le menu + + option.dataset.details = JSON.stringify({ + rue: lieu.rue, + codePostal: lieu.codePostal, + latitude: lieu.latitude, + longitude: lieu.longitude + }); + lieuSelect.appendChild(option); }); lieuSelect.disabled = false; // Activer le menu des lieux diff --git a/src/Controller/LieuController.php b/src/Controller/LieuController.php index dba6b40..b790405 100644 --- a/src/Controller/LieuController.php +++ b/src/Controller/LieuController.php @@ -30,17 +30,22 @@ class LieuController extends AbstractController ): JsonResponse { $data = json_decode($request->getContent(), true); - - if (!isset($data['nom'], $data['rue'], $data['latitude'], $data['longitude'], $data['villeId'])) { - return new JsonResponse(['error' => 'Données manquantes'], Response::HTTP_BAD_REQUEST); + if (!isset($data['nom'], $data['latitude'], $data['longitude'], $data['villeId'])) { + return new JsonResponse(['error' => 'Données manquantes.'], Response::HTTP_BAD_REQUEST); } - $ville = $villeRepository->find($data['villeId']); if (!$ville) { - return new JsonResponse(['error' => 'Ville non trouvée'], Response::HTTP_NOT_FOUND); + return new JsonResponse(['error' => 'Ville non trouvée.'], Response::HTTP_NOT_FOUND); } + $existingLieuByName = $entityManager->getRepository(Lieu::class)->findOneBy([ + 'nom' => $data['nom'], + 'ville' => $ville, + ]); + if ($existingLieuByName) { + return new JsonResponse(['error' => "Un lieu avec le nom '{$data['nom']}' existe déjà."], Response::HTTP_CONFLICT); + } $lieu = new Lieu(); $lieu->setNom($data['nom']); @@ -49,20 +54,16 @@ class LieuController extends AbstractController $lieu->setLongitude($data['longitude']); $lieu->setVille($ville); - $entityManager->persist($lieu); $entityManager->flush(); - return new JsonResponse([ 'id' => $lieu->getIdLieu(), 'nom' => $lieu->getNom(), - 'rue' => $lieu->getRue(), - 'latitude' => $lieu->getLatitude(), - 'longitude' => $lieu->getLongitude(), ], Response::HTTP_CREATED); } + #[Route('/get-bounds/{villeId}', name: 'get_bounds', methods: ['GET'])] public function getBounds(VilleRepository $villeRepository, string $villeId): JsonResponse { diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php index 6fc488e..878ff6c 100644 --- a/src/Controller/MainController.php +++ b/src/Controller/MainController.php @@ -8,23 +8,40 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\HttpFoundation\Request; class MainController extends AbstractController { #[Route('/', name: 'home')] - public function index(TokenStorageInterface $tokenStorage, SortieRepository $sortieRepository, SiteRepository $siteRepository): Response - { + public function index( + TokenStorageInterface $tokenStorage, + SortieRepository $sortieRepository, + Request $request + ): Response { $token = $tokenStorage->getToken(); $userConnect = $token?->getUser(); - $sorties = $sortieRepository->findAll(); - $sites = $siteRepository->findAll(); + + // Récupérer les paramètres de filtre + $search = $request->query->get('search', ''); + $siteId = $request->query->get('site', ''); + $startDate = $request->query->get('start_date', ''); + $endDate = $request->query->get('end_date', ''); + $organisateur = $request->query->get('organisateur', false); + $inscrit = $request->query->get('inscrit', false); + $nonInscrit = $request->query->get('non_inscrit', false); + $passees = $request->query->get('passees', false); + + $sorties = $sortieRepository->findWithFilters($search, $siteId, $startDate, $endDate, $organisateur, $inscrit, $nonInscrit, $passees, $userConnect); + return $this->render('main/index.html.twig', [ - 'sites' => $sites, 'profile' => $userConnect, 'sorties' => $sorties, + 'sites' => $sortieRepository->findAllSites(), ]); } + + #[Route('/inscription', name: 'inscription')] public function inscription(TokenStorageInterface $tokenStorage): Response { diff --git a/src/Controller/SortieController.php b/src/Controller/SortieController.php index 2c4afb1..9874a84 100644 --- a/src/Controller/SortieController.php +++ b/src/Controller/SortieController.php @@ -24,7 +24,7 @@ class SortieController extends AbstractController TokenStorageInterface $tokenStorage, LieuRepository $lieuRepository, ParticipantRepository $participantRepository, - EtatRepository $etatRepository // Ajout du repository pour les états + EtatRepository $etatRepository ): Response { $sortie = new Sortie(); @@ -63,7 +63,6 @@ class SortieController extends AbstractController } $sortie->setSite($participant->getSite()); - $sortie->setParticipant($participant); // Récupérer l'état "en création" avec l'ID donné $etat = $etatRepository->find('019349ba-38ca-7a39-93c3-62f046671525'); @@ -75,6 +74,9 @@ class SortieController extends AbstractController // Associer l'état à la sortie $sortie->setEtat($etat); + // Associer l'organisateur à la sortie + $sortie->setOrganisateur($participant); + // Sauvegarder la sortie $entityManager->persist($sortie); $entityManager->flush(); @@ -89,4 +91,128 @@ class SortieController extends AbstractController 'form' => $form->createView(), ]); } + + #[Route('/view/{id}', name: 'view', methods: ['GET'])] + public function view(string $id, EntityManagerInterface $entityManager, TokenStorageInterface $tokenStorage): Response + { + // Récupérer l'utilisateur connecté + $token = $tokenStorage->getToken(); + $userConnect = $token?->getUser(); + + $sortie = $entityManager->getRepository(Sortie::class)->find($id); + + if (!$sortie) { + $this->addFlash('error', 'La sortie demandée n\'existe pas.'); + return $this->redirectToRoute('home'); + } + + // Récupérer le profil de l'utilisateur connecté + $profile = $this->getUser(); + + return $this->render('sortie/view.html.twig', [ + 'sortie' => $sortie, + 'profile' => $userConnect, + ]); + } + + #[Route('/inscription/{id}', name: 'inscription', methods: ['POST'])] + public function inscription(string $id, EntityManagerInterface $entityManager, TokenStorageInterface $tokenStorage): Response + { + // Récupérer l'utilisateur connecté + $token = $tokenStorage->getToken(); + $userConnect = $token?->getUser(); + + if (!$userConnect) { + $this->addFlash('error', 'Vous devez être connecté pour vous inscrire.'); + return $this->redirectToRoute('app_login'); + } + + // Récupérer la sortie + $sortie = $entityManager->getRepository(Sortie::class)->find($id); + + if (!$sortie) { + $this->addFlash('error', 'La sortie demandée n\'existe pas.'); + return $this->redirectToRoute('home'); + } + + // Vérifier que la sortie est "Ouverte" + if ($sortie->getEtat()->getLibelle() !== 'Ouverte') { + $this->addFlash('error', 'Vous ne pouvez pas vous inscrire à cette sortie car elle n\'est pas ouverte.'); + return $this->redirectToRoute('sortie_view', ['id' => $id]); + } + + // Vérifier si l'utilisateur est déjà inscrit + if ($sortie->getParticipants()->contains($userConnect)) { + $this->addFlash('error', 'Vous êtes déjà inscrit à cette sortie.'); + return $this->redirectToRoute('sortie_view', ['id' => $id]); + } + + // Vérifier le nombre maximum d'inscriptions + if ($sortie->getParticipants()->count() >= $sortie->getNbInscriptionsMax()) { + $this->addFlash('error', 'Le nombre maximum d\'inscriptions a été atteint pour cette sortie.'); + return $this->redirectToRoute('sortie_view', ['id' => $id]); + } + + // Ajouter l'utilisateur à la liste des participants + $sortie->addParticipant($userConnect); + $entityManager->flush(); + + $this->addFlash('success', 'Vous êtes inscrit à la sortie avec succès !'); + + return $this->redirectToRoute('sortie_view', ['id' => $id]); + } + + #[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])] + public function edit( + string $id, + Request $request, + EntityManagerInterface $entityManager, + TokenStorageInterface $tokenStorage, + LieuRepository $lieuRepository + ): Response { + // Récupérer la sortie + $sortie = $entityManager->getRepository(Sortie::class)->find($id); + + if (!$sortie) { + $this->addFlash('error', 'La sortie demandée n\'existe pas.'); + return $this->redirectToRoute('home'); + } + + // Vérifier si l'utilisateur est l'organisateur + $token = $tokenStorage->getToken(); + $userConnect = $token?->getUser(); + + if ($userConnect->getIdParticipant() !== $sortie->getOrganisateur()->getIdParticipant()) { + $this->addFlash('error', 'Vous n\'avez pas l\'autorisation de modifier cette sortie.'); + return $this->redirectToRoute('home'); + } + + // Créer le formulaire + $form = $this->createForm(SortieType::class, $sortie); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Mettre à jour le lieu si modifié + $lieuId = $form->get('lieu')->getData(); + $lieu = $lieuRepository->find($lieuId); + + if ($lieu) { + $sortie->setLieu($lieu); + } + + // Sauvegarder les modifications + $entityManager->flush(); + + $this->addFlash('success', 'La sortie a été mise à jour avec succès.'); + + return $this->redirectToRoute('sortie_view', ['id' => $sortie->getIdSortie()]); + } + + return $this->render('sortie/edit.html.twig', [ + 'form' => $form->createView(), + 'sortie' => $sortie, + 'profile' => $userConnect, + ]); + } + } diff --git a/src/Form/SortieType.php b/src/Form/SortieType.php index 8746166..fae6236 100644 --- a/src/Form/SortieType.php +++ b/src/Form/SortieType.php @@ -31,6 +31,7 @@ class SortieType extends AbstractType 'label' => 'Date et heure de début', 'widget' => 'single_text', 'html5' => true, + 'input' => 'datetime_immutable', 'attr' => ['class' => 'form-control'], ]) ->add('duree', IntegerType::class, [ @@ -41,6 +42,7 @@ class SortieType extends AbstractType 'label' => "Date limite d'inscription", 'widget' => 'single_text', 'html5' => true, + 'input' => 'datetime_immutable', 'attr' => ['class' => 'form-control'], ]) ->add('nbInscriptionsMax', IntegerType::class, [ diff --git a/src/Repository/SortieRepository.php b/src/Repository/SortieRepository.php index e497d58..f1135c2 100644 --- a/src/Repository/SortieRepository.php +++ b/src/Repository/SortieRepository.php @@ -2,6 +2,7 @@ namespace App\Repository; +use App\Entity\Site; use App\Entity\Sortie; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -16,6 +17,64 @@ class SortieRepository extends ServiceEntityRepository parent::__construct($registry, Sortie::class); } + public function findWithFilters($search, $siteId, $startDate, $endDate, $organisateur, $inscrit, $nonInscrit, $passees, $user) + { + $qb = $this->createQueryBuilder('s'); + + if ($search) { + $qb->andWhere('s.nom LIKE :search') + ->setParameter('search', '%' . $search . '%'); + } + + if ($siteId) { + $qb->andWhere('s.site = :siteId') + ->setParameter('siteId', $siteId); + } + + if ($startDate) { + $qb->andWhere('s.dateHeureDebut >= :startDate') + ->setParameter('startDate', new \DateTime($startDate)); + } + + if ($endDate) { + $qb->andWhere('s.dateHeureDebut <= :endDate') + ->setParameter('endDate', new \DateTime($endDate)); + } + + if ($organisateur) { + $qb->andWhere('s.organisateur = :user') + ->setParameter('user', $user); + } + + if ($inscrit) { + $qb->andWhere(':user MEMBER OF s.participants') + ->setParameter('user', $user); + } + + if ($nonInscrit) { + $qb->andWhere(':user NOT MEMBER OF s.participants') + ->setParameter('user', $user); + } + + if ($passees) { + $qb->andWhere('s.dateHeureDebut < :now') + ->setParameter('now', new \DateTime()); + } else { + $qb->andWhere('s.dateHeureDebut >= :now') + ->setParameter('now', new \DateTime()); + } + + return $qb->getQuery()->getResult(); + } + + public function findAllSites() + { + return $this->getEntityManager() + ->getRepository(Site::class) + ->findAll(); + } + + // /** // * @return Sortie[] Returns an array of Sortie objects // */ diff --git a/templates/main/index.html.twig b/templates/main/index.html.twig index 32080ba..86a3391 100644 --- a/templates/main/index.html.twig +++ b/templates/main/index.html.twig @@ -10,71 +10,89 @@ {% endblock %} {% block content %} -
-

Liste des sorties

+
+

🎉 Liste des sorties

+ + +
+
+ +
+ + +
- - -
- - + {% for site in sites %} - + {% endfor %}
- -
- - -
-
- - + +
- - + +
-
- - +
+ +
-
- - + +
+ +
-
- - + +
+ +
-
- - + +
+ +
- -
-
-
+ +
Créer une sortie @@ -83,7 +101,7 @@ {% for sortie in sorties %}
-

{{ sortie.nom }}

+

{{ sortie.nom }}

Date de début : {{ sortie.dateHeureDebut|date('d/m/Y H:i') }}
Durée : {{ sortie.duree }} minutes
@@ -91,9 +109,11 @@ Infos : {{ sortie.infosSortie }}

+ {% else %} +

Aucune sortie ne correspond à vos critères.

{% endfor %}
diff --git a/templates/sortie/create.html.twig b/templates/sortie/create.html.twig index 8cbc6c8..155a30c 100644 --- a/templates/sortie/create.html.twig +++ b/templates/sortie/create.html.twig @@ -46,6 +46,10 @@ .modern-button svg { margin-right: 0.5rem; } + #add-lieu-modal .bg-white { + max-width: 80%; + max-height: 90%; + } {% endblock %} @@ -58,6 +62,7 @@ {{ form_start(form, { 'attr': { 'class': 'space-y-6' } }) }} +
@@ -137,24 +142,31 @@ Annuler
- + {{ form_rest(form) }} {{ form_end(form, { 'render_rest': false }) }}
-