feat: implement educator management and display educator initials on training schedule
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Educateur;
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.entity.Equipe;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import com.astalange.core.repository.EducateurRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/educateurs")
|
||||
public class EducateurController {
|
||||
|
||||
private final EducateurRepository educateurRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
|
||||
public EducateurController(EducateurRepository educateurRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
EquipeRepository equipeRepository,
|
||||
SaisonRepository saisonRepository) {
|
||||
this.educateurRepository = educateurRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listEducateurs(Model model) {
|
||||
model.addAttribute("educateurs", educateurRepository.findAllWithCategorieAndEquipe());
|
||||
return "parametrage/educateurs_list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("educateur", new Educateur());
|
||||
populateModelAttributes(model);
|
||||
return "parametrage/educateurs_form";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
Educateur educateur = educateurRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid educator ID: " + id));
|
||||
model.addAttribute("educateur", educateur);
|
||||
populateModelAttributes(model);
|
||||
return "parametrage/educateurs_form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String saveEducateur(@ModelAttribute("educateur") Educateur educateur) {
|
||||
if (educateur.getCategorie() != null && educateur.getCategorie().getId() == null) {
|
||||
educateur.setCategorie(null);
|
||||
}
|
||||
if (educateur.getEquipe() != null && educateur.getEquipe().getId() == null) {
|
||||
educateur.setEquipe(null);
|
||||
}
|
||||
educateurRepository.save(educateur);
|
||||
return "redirect:/educateurs";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteEducateur(@PathVariable Long id) {
|
||||
Educateur educateur = educateurRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid educator ID: " + id));
|
||||
educateurRepository.delete(educateur);
|
||||
return "redirect:/educateurs";
|
||||
}
|
||||
|
||||
private void populateModelAttributes(Model model) {
|
||||
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||
if (saisonActive != null) {
|
||||
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
|
||||
model.addAttribute("equipes", equipeRepository.findBySaison(saisonActive));
|
||||
} else {
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
model.addAttribute("equipes", equipeRepository.findAllWithCategorie());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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.EducateurRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.repository.TerrainRepository;
|
||||
@@ -23,17 +24,20 @@ public class PlanningController {
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
private final EducateurRepository educateurRepository;
|
||||
|
||||
public PlanningController(CreneauEntrainementRepository creneauRepository,
|
||||
TerrainRepository terrainRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
SaisonRepository saisonRepository,
|
||||
EquipeRepository equipeRepository) {
|
||||
EquipeRepository equipeRepository,
|
||||
EducateurRepository educateurRepository) {
|
||||
this.creneauRepository = creneauRepository;
|
||||
this.terrainRepository = terrainRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
this.educateurRepository = educateurRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -56,10 +60,33 @@ public class PlanningController {
|
||||
}
|
||||
|
||||
List<CreneauEntrainement> creneaux = creneauRepository.findBySaisonAndJourSemaineFetch(saisonActive, selectedDay);
|
||||
List<Educateur> educateurs = educateurRepository.findAllWithCategorieAndEquipe();
|
||||
|
||||
// Detect conflicts
|
||||
// Detect conflicts and populate educator initials
|
||||
for (int i = 0; i < creneaux.size(); i++) {
|
||||
CreneauEntrainement c1 = creneaux.get(i);
|
||||
|
||||
// Populate educator initials
|
||||
List<String> initials = new ArrayList<>();
|
||||
for (Educateur ed : educateurs) {
|
||||
boolean match = false;
|
||||
if (c1.getEquipe() != null) {
|
||||
if (ed.getEquipe() != null && ed.getEquipe().getId().equals(c1.getEquipe().getId())) {
|
||||
match = true;
|
||||
}
|
||||
} else if (c1.getCategorie() != null) {
|
||||
if (ed.getCategorie() != null && ed.getCategorie().getId().equals(c1.getCategorie().getId())) {
|
||||
match = true;
|
||||
} else if (ed.getEquipe() != null && ed.getEquipe().getCategorie() != null && ed.getEquipe().getCategorie().getId().equals(c1.getCategorie().getId())) {
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
initials.add(ed.getInitiales());
|
||||
}
|
||||
}
|
||||
c1.setInitialesEducateurs(String.join(", ", initials));
|
||||
|
||||
for (int j = 0; j < creneaux.size(); j++) {
|
||||
if (i == j) continue;
|
||||
CreneauEntrainement c2 = creneaux.get(j);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<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>
|
||||
<a href="/equipes" th:classappend="${requestURI != null and requestURI.startsWith('/equipes') ? '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">Équipes</a>
|
||||
<a href="/educateurs" th:classappend="${requestURI != null and requestURI.startsWith('/educateurs') ? '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">Éducateurs</a>
|
||||
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? '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">Modes de Paiement</a>
|
||||
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? '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">Équipements</a>
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Éducateur - 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 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="${educateur.id == null ? 'Nouvel Éducateur' : 'Modifier Éducateur'}">Éducateur</h2>
|
||||
</header>
|
||||
|
||||
<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="@{/educateurs}" th:object="${educateur}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Nom <span class="text-red-500">*</span></label>
|
||||
<input type="text" th:field="*{nom}" 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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Prénom <span class="text-red-500">*</span></label>
|
||||
<input type="text" th:field="*{prenom}" 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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Numéro de Téléphone (non obligatoire)</label>
|
||||
<input type="tel" th:field="*{telephone}" 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 font-semibold">Adresse E-mail (facultatif)</label>
|
||||
<input type="email" th:field="*{email}" 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 class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Catégorie</label>
|
||||
<select th:field="*{categorie}" 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="">-- Aucune --</option>
|
||||
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom}">U15</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Équipe</label>
|
||||
<select th:field="*{equipe}" 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="">-- Aucune --</option>
|
||||
<option th:each="equip : ${equipes}" th:value="${equip.id}" th:text="${equip.nom + ' (' + equip.categorie.nom + ')'}">Équipe A (U15)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<a th:href="@{/educateurs}" 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,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paramétrage - Éducateurs</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 class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Éducateurs / Entraîneurs</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Liste des Éducateurs</h3>
|
||||
<a th:href="@{/educateurs/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||
+ Nouvel Éducateur
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<th class="py-3 px-6 font-medium text-left">Nom</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Prénom</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Téléphone</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Email</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Catégorie</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Équipe</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(educateurs)}">
|
||||
<td colspan="7" class="py-8 text-center text-gray-500">Aucun éducateur n'est paramétré.</td>
|
||||
</tr>
|
||||
<tr th:each="ed : ${educateurs}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${ed.nom}">Dupont</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${ed.prenom}">Jean</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${ed.telephone != null and !ed.telephone.isEmpty() ? ed.telephone : '-'}">-</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${ed.email != null and !ed.email.isEmpty() ? ed.email : '-'}">-</td>
|
||||
<td class="py-4 px-6 text-gray-600">
|
||||
<span th:if="${ed.categorie != null}" th:text="${ed.categorie.nom}" class="px-2.5 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">U15</span>
|
||||
<span th:if="${ed.categorie == null}">-</span>
|
||||
</td>
|
||||
<td class="py-4 px-6 text-gray-600">
|
||||
<span th:if="${ed.equipe != null}" th:text="${ed.equipe.nom}" class="px-2.5 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800">Équipe A</span>
|
||||
<span th:if="${ed.equipe == null}">-</span>
|
||||
</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||
<a th:href="@{/educateurs/{id}/edit(id=${ed.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="@{/educateurs/{id}/delete(id=${ed.id})}" method="post" onsubmit="return confirm('Supprimer cet éducateur ?');">
|
||||
<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>
|
||||
@@ -147,7 +147,7 @@
|
||||
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 + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U15</span>
|
||||
<span class="font-bold text-xs" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '') + (c.initialesEducateurs != null and !c.initialesEducateurs.isEmpty() ? ' [' + c.initialesEducateurs + ']' : '')}">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>
|
||||
@@ -192,7 +192,13 @@
|
||||
<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 + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U11</td>
|
||||
<td class="py-4 px-6 text-gray-900">
|
||||
<span class="font-semibold" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U11</span>
|
||||
<span th:if="${c.initialesEducateurs != null and !c.initialesEducateurs.isEmpty()}"
|
||||
th:text="${c.initialesEducateurs}"
|
||||
class="ml-2 px-2.5 py-1 bg-blue-100 text-blue-800 rounded text-xs font-semibold"
|
||||
th:title="'Éducateurs : ' + ${c.initialesEducateurs}">JD</span>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user