feat: add weekly training schedule planning with Gantt chart and conflict detection

This commit is contained in:
2026-05-30 00:13:55 +02:00
parent a429e67133
commit 0baca4a334
12 changed files with 933 additions and 0 deletions
@@ -0,0 +1,158 @@
package com.astalange.web.controller;
import com.astalange.core.entity.*;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.CreneauEntrainementRepository;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.repository.TerrainRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.*;
@Controller
@RequestMapping("/planning")
public class PlanningController {
private final CreneauEntrainementRepository creneauRepository;
private final TerrainRepository terrainRepository;
private final CategorieRepository categorieRepository;
private final SaisonRepository saisonRepository;
public PlanningController(CreneauEntrainementRepository creneauRepository,
TerrainRepository terrainRepository,
CategorieRepository categorieRepository,
SaisonRepository saisonRepository) {
this.creneauRepository = creneauRepository;
this.terrainRepository = terrainRepository;
this.categorieRepository = categorieRepository;
this.saisonRepository = saisonRepository;
}
@GetMapping
public String showPlanning(@RequestParam(required = false) JourSemaine jour, Model model) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive == null) {
model.addAttribute("noActiveSeason", true);
return "planning/planning";
}
JourSemaine selectedDay = (jour != null) ? jour : JourSemaine.LUNDI;
List<Terrain> terrains = terrainRepository.findAll();
// Sort terrains to ensure consistent order (Terrain Vert then Terrain Synthétique)
terrains.sort(Comparator.comparing(Terrain::getId));
Map<Long, Integer> terrainIndices = new HashMap<>();
for (int i = 0; i < terrains.size(); i++) {
terrainIndices.put(terrains.get(i).getId(), i);
}
List<CreneauEntrainement> creneaux = creneauRepository.findBySaisonAndJourSemaineFetch(saisonActive, selectedDay);
// Detect conflicts
for (int i = 0; i < creneaux.size(); i++) {
CreneauEntrainement c1 = creneaux.get(i);
for (int j = 0; j < creneaux.size(); j++) {
if (i == j) continue;
CreneauEntrainement c2 = creneaux.get(j);
if (c1.overlapsWith(c2)) {
c1.setEnConflit(true);
c1.getCategoriesEnConflit().add(c2.getCategorie().getNom());
}
}
}
model.addAttribute("saisonActive", saisonActive);
model.addAttribute("selectedDay", selectedDay);
model.addAttribute("jours", JourSemaine.values());
model.addAttribute("terrains", terrains);
model.addAttribute("terrainIndices", terrainIndices);
model.addAttribute("creneaux", creneaux);
return "planning/planning";
}
@GetMapping("/new")
public String showCreateForm(Model model, RedirectAttributes redirectAttributes) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive == null) {
redirectAttributes.addFlashAttribute("error", "Veuillez activer une saison avant de planifier un créneau.");
return "redirect:/saisons";
}
CreneauEntrainement creneau = new CreneauEntrainement();
creneau.setSaison(saisonActive);
model.addAttribute("creneau", creneau);
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("terrains", terrainRepository.findAll());
model.addAttribute("zones", ZoneTerrain.values());
model.addAttribute("jours", JourSemaine.values());
return "planning/form";
}
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
CreneauEntrainement creneau = creneauRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Créneau introuvable : " + id));
model.addAttribute("creneau", creneau);
model.addAttribute("categories", categorieRepository.findBySaison(creneau.getSaison()));
model.addAttribute("terrains", terrainRepository.findAll());
model.addAttribute("zones", ZoneTerrain.values());
model.addAttribute("jours", JourSemaine.values());
return "planning/form";
}
@PostMapping
public String saveCreneau(@ModelAttribute("creneau") CreneauEntrainement creneau, BindingResult result, Model model, RedirectAttributes redirectAttributes) {
// Validation
if (creneau.getHeureDebut() != null && creneau.getHeureFin() != null) {
if (!creneau.getHeureDebut().isBefore(creneau.getHeureFin())) {
result.rejectValue("heureDebut", "error.heureDebut", "L'heure de début doit être antérieure à l'heure de fin.");
}
if (creneau.getHeureDebut().isBefore(java.time.LocalTime.of(8, 0)) || creneau.getHeureFin().isAfter(java.time.LocalTime.of(22, 0))) {
result.rejectValue("heureDebut", "error.heureDebut", "Les entraînements doivent se dérouler entre 08:00 et 22:00.");
}
}
if (result.hasErrors()) {
Saison saison = creneau.getSaison();
if (saison == null) {
saison = saisonRepository.findByEstActiveTrue().orElseThrow();
creneau.setSaison(saison);
}
model.addAttribute("categories", categorieRepository.findBySaison(saison));
model.addAttribute("terrains", terrainRepository.findAll());
model.addAttribute("zones", ZoneTerrain.values());
model.addAttribute("jours", JourSemaine.values());
return "planning/form";
}
// Set active season if not set (for new items)
if (creneau.getSaison() == null) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElseThrow();
creneau.setSaison(saisonActive);
}
creneauRepository.save(creneau);
redirectAttributes.addFlashAttribute("success", "Créneau d'entraînement enregistré avec succès.");
return "redirect:/planning?jour=" + creneau.getJourSemaine().name();
}
@PostMapping("/{id}/delete")
public String deleteCreneau(@PathVariable Long id, RedirectAttributes redirectAttributes) {
CreneauEntrainement creneau = creneauRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Créneau introuvable : " + id));
JourSemaine jour = creneau.getJourSemaine();
creneauRepository.delete(creneau);
redirectAttributes.addFlashAttribute("success", "Créneau d'entraînement supprimé avec succès.");
return "redirect:/planning?jour=" + jour.name();
}
}
@@ -12,6 +12,8 @@
<a href="/" th:classappend="${requestURI == '/' ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Tableau de bord</a>
<a href="/adherents" th:classappend="${requestURI != null and requestURI.startsWith('/adherents') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Adhérents</a>
<a href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Paiements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Planning</div>
<a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Entraînements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Saisons</a>
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Catégories</a>
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Créneau d'Entraînement - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> body { font-family: 'Inter', sans-serif; } </style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<!-- Header -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
<h2 class="text-lg font-semibold text-gray-800" th:text="${creneau.id == null ? 'Ajouter un Créneau d''Entraînement' : 'Modifier le Créneau d''Entraînement'}">Créneau</h2>
</header>
<!-- Form content -->
<div class="flex-1 overflow-auto p-6">
<div class="max-w-xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<form th:action="@{/planning}" th:object="${creneau}" method="post" class="space-y-6">
<input type="hidden" th:field="*{id}" />
<input type="hidden" th:field="*{saison.id}" th:if="${creneau.saison != null}" />
<!-- Catégorie -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie <span class="text-red-500">*</span></label>
<select th:field="*{categorie.id}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez une catégorie</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom}">U15</option>
</select>
</div>
<!-- Terrain -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Terrain <span class="text-red-500">*</span></label>
<select th:field="*{terrain.id}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez un terrain</option>
<option th:each="t : ${terrains}" th:value="${t.id}" th:text="${t.nom}">Terrain Vert</option>
</select>
</div>
<!-- Zone de terrain -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Zone de terrain <span class="text-red-500">*</span></label>
<select th:field="*{zone}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez la zone occupée</option>
<option th:each="z : ${zones}" th:value="${z.name()}" th:text="${z.libelle}">Terrain Complet</option>
</select>
</div>
<!-- Jour de la semaine -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Jour de la semaine <span class="text-red-500">*</span></label>
<select th:field="*{jourSemaine}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez le jour</option>
<option th:each="j : ${jours}" th:value="${j.name()}" th:text="${j.libelle}">Lundi</option>
</select>
</div>
<!-- Horaires (Début & Fin) -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Heure de début <span class="text-red-500">*</span></label>
<input type="time" th:field="*{heureDebut}" required min="08:00" max="22:00"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Heure de fin <span class="text-red-500">*</span></label>
<input type="time" th:field="*{heureFin}" required min="08:00" max="22:00"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<!-- Custom Validation Error Display -->
<div th:if="${#fields.hasErrors('heureDebut')}" class="text-red-500 text-xs font-semibold mt-1">
<p th:errors="*{heureDebut}"></p>
</div>
<!-- Info notice -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 text-xs text-gray-500">
<p class="font-semibold text-gray-700 mb-1">Information sur les plages horaires :</p>
<p>Les entraînements doivent être planifiés entre 08:00 et 22:00.</p>
<p class="mt-1">Les éventuels conflits de superposition d'horaires et de zones de terrain seront calculés et signalés automatiquement sur le tableau de bord.</p>
</div>
<!-- Actions -->
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
<a th:href="@{/planning}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
</div>
</form>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Planning des Entraînements - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.gantt-grid {
display: grid;
grid-template-columns: 40px 120px repeat(56, minmax(12px, 1fr));
}
</style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<!-- Header -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
<h2 class="text-lg font-semibold text-gray-800">Planning & Occupation des Terrains</h2>
<div class="flex items-center space-x-2" th:if="${saisonActive != null}">
<span class="text-xs bg-green-100 text-green-800 font-semibold px-2.5 py-0.5 rounded-full" th:text="${'Saison Active : ' + saisonActive.nom}">Saison Actuelle</span>
</div>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6 bg-gray-50/50">
<!-- Warnings / Errors -->
<div th:if="${noActiveSeason}" class="bg-amber-50 border-l-4 border-amber-500 p-4 mb-6 rounded-lg">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-amber-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-amber-700 font-medium">
Aucune saison active. Veuillez <a href="/saisons" class="font-bold underline hover:text-amber-800">activer une saison</a> dans le paramétrage pour planifier les entraînements.
</p>
</div>
</div>
</div>
<div th:if="${success}" class="bg-green-50 border-l-4 border-green-500 p-4 mb-6 rounded-lg">
<p class="text-sm text-green-700 font-medium" th:text="${success}"></p>
</div>
<!-- Page actions -->
<div class="flex justify-between items-center mb-6" th:if="${saisonActive != null}">
<h3 class="text-xl font-bold text-gray-900">Planning Hebdomadaire</h3>
<a th:href="@{/planning/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 shadow transition-colors">
+ Ajouter un Créneau
</a>
</div>
<!-- Tabs: Days of the week -->
<div class="mb-6 border-b border-gray-200" th:if="${saisonActive != null}">
<nav class="-mb-px flex space-x-6">
<a th:each="j : ${jours}"
th:href="@{/planning(jour=${j.name()})}"
th:classappend="${j == selectedDay} ? 'border-blue-600 text-blue-600 font-semibold' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm transition-all"
th:text="${j.libelle}">
Lundi
</a>
</nav>
</div>
<!-- Gantt view -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-8 p-6" th:if="${saisonActive != null}">
<div class="overflow-x-auto">
<div class="gantt-grid min-w-[900px] border border-gray-200 rounded-lg bg-white relative">
<!-- Top Hour row -->
<div class="col-span-2 text-xs font-semibold text-gray-500 bg-gray-50 p-3 border-b flex items-center justify-center border-r">Terrain / Zone</div>
<div th:each="hour : ${#numbers.sequence(8, 21)}"
style="grid-column: span 4;"
class="text-xs font-semibold text-gray-500 bg-gray-50 py-3 border-b border-l text-center"
th:text="${#strings.concat(#numbers.formatInteger(hour, 2), ':00')}">
08:00
</div>
<!-- Terrain Vert Block -->
<!-- Full width Banner -->
<div class="col-span-58 bg-blue-50/50 text-blue-800 font-bold px-4 py-1 text-xs uppercase tracking-wider flex items-center border-b" style="grid-row: 2;">
Terrain Vert
</div>
<!-- Quarters -->
<!-- Row 3: Quart 1 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 3;">V</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 3;">Quart 1</div>
<!-- Row 4: Quart 2 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 4;">V</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 4;">Quart 2</div>
<!-- Row 5: Quart 3 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 5;">V</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 5;">Quart 3</div>
<!-- Row 6: Quart 4 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 6;">V</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 6;">Quart 4</div>
<!-- Terrain Synthétique Block -->
<!-- Full width Banner -->
<div class="col-span-58 bg-purple-50/50 text-purple-800 font-bold px-4 py-1 text-xs uppercase tracking-wider flex items-center border-b border-t" style="grid-row: 7;">
Terrain Synthétique
</div>
<!-- Quarters -->
<!-- Row 8: Quart 1 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 8;">S</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 8;">Quart 1</div>
<!-- Row 9: Quart 2 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 9;">S</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 9;">Quart 2</div>
<!-- Row 10: Quart 3 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 10;">S</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 10;">Quart 3</div>
<!-- Row 11: Quart 4 -->
<div class="bg-gray-50 text-[10px] font-bold text-gray-500 flex items-center justify-center border-b border-r" style="grid-column: 1; grid-row: 11;">S</div>
<div class="bg-gray-50 text-xs font-medium text-gray-600 pl-3 py-3 border-b border-r flex items-center" style="grid-column: 2; grid-row: 11;">Quart 4</div>
<!-- Background vertical lines to build timeline slots -->
<!-- Terrain Vert rows (3, 4, 5, 6) -->
<div th:each="r : ${#numbers.sequence(3, 6)}" class="border-b border-gray-100 flex h-14" th:style="'grid-column: 3 / 59; grid-row: ' + ${r} + '; pointer-events: none;'">
<div th:each="i : ${#numbers.sequence(1, 14)}" class="flex-1 border-l border-gray-100/70 last:border-r"></div>
</div>
<!-- Terrain Synthétique rows (8, 9, 10, 11) -->
<div th:each="r : ${#numbers.sequence(8, 11)}" class="border-b border-gray-100 flex h-14" th:style="'grid-column: 3 / 59; grid-row: ' + ${r} + '; pointer-events: none;'">
<div th:each="i : ${#numbers.sequence(1, 14)}" class="flex-1 border-l border-gray-100/70 last:border-r"></div>
</div>
<!-- Scheduled slots rendered dynamically -->
<div th:each="c : ${creneaux}"
th:style="'grid-column: ' + ${c.getStartColumn()} + ' / ' + ${c.getEndColumn()} + '; grid-row: ' + ${c.getStartRow(terrainIndices.get(c.terrain.id))} + ' / ' + ${c.getEndRow(terrainIndices.get(c.terrain.id))} + ';'"
th:classappend="${c.enConflit} ? 'bg-red-50 border-red-500 text-red-800 hover:bg-red-100' : 'bg-blue-50 border-blue-500 text-blue-800 hover:bg-blue-100'"
class="border-l-4 p-2 rounded shadow-sm flex flex-col justify-between overflow-hidden cursor-pointer hover:shadow transition-all relative z-10 m-0.5">
<div class="flex justify-between items-start">
<span class="font-bold text-xs" th:text="${c.categorie.nom}">U15</span>
<!-- Delete button or info -->
<div class="flex items-center space-x-1">
<a th:href="@{/planning/{id}/edit(id=${c.id})}" class="text-[10px] text-gray-500 hover:underline">Modifier</a>
</div>
</div>
<div class="text-[10px] font-medium mt-1">
<span th:text="${c.heureDebut} + ' - ' + ${c.heureFin}">18:00 - 19:30</span>
<span class="text-gray-400 font-normal"> | </span>
<span th:text="${c.zone.libelle}">Demi-terrain A</span>
</div>
<!-- Conflict warning -->
<div th:if="${c.enConflit}" class="absolute bottom-1 right-1 text-red-500"
th:title="'Conflit d\'espace/temps avec : ' + ${#strings.listJoin(c.categoriesEnConflit, ', ')}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
</div>
</div>
</div>
</div>
</div>
<!-- List view below Gantt -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden" th:if="${saisonActive != null}">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900" th:text="${'Liste des entraînements - ' + selectedDay.libelle}">Liste des entraînements - Lundi</h3>
</div>
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50/50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
<th class="py-3 px-6 font-medium">Catégorie</th>
<th class="py-3 px-6 font-medium">Terrain</th>
<th class="py-3 px-6 font-medium">Zone</th>
<th class="py-3 px-6 font-medium">Horaires</th>
<th class="py-3 px-6 font-medium text-center">Statut</th>
<th class="py-3 px-6 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 text-sm">
<tr th:if="${#lists.isEmpty(creneaux)}">
<td colspan="6" class="py-8 text-center text-gray-500">Aucun entraînement planifié pour ce jour.</td>
</tr>
<tr th:each="c : ${creneaux}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-semibold text-gray-900" th:text="${c.categorie.nom}">U11</td>
<td class="py-4 px-6 text-gray-600" th:text="${c.terrain.nom}">Terrain Synthétique</td>
<td class="py-4 px-6 text-gray-600">
<span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs" th:text="${c.zone.libelle}">Demi A</span>
</td>
<td class="py-4 px-6 text-gray-600 font-medium" th:text="${c.heureDebut + ' - ' + c.heureFin}">14:00 - 15:30</td>
<td class="py-4 px-6 text-center">
<span th:if="${c.enConflit}"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"
th:title="'Conflit avec : ' + ${#strings.listJoin(c.categoriesEnConflit, ', ')}">
Conflit
</span>
<span th:unless="${c.enConflit}" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Disponible
</span>
</td>
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
<a th:href="@{/planning/{id}/edit(id=${c.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
<form th:action="@{/planning/{id}/delete(id=${c.id})}" method="post" onsubmit="return confirm('Supprimer ce créneau d\'entraînement ?');">
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>