feat: add admin toggle to open/close public pre-registrations with ui messaging
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m52s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m18s

This commit is contained in:
2026-06-12 23:37:41 +02:00
parent bb24363500
commit 87c65c6ebd
6 changed files with 77 additions and 18 deletions
@@ -17,6 +17,9 @@ public class Saison {
@Column(name = "est_active", nullable = false)
private Boolean estActive = false;
@Column(name = "inscriptions_ouvertes", nullable = false)
private Boolean inscriptionsOuvertes = false;
// Getters and Setters
public Long getId() {
@@ -43,10 +46,20 @@ public class Saison {
this.estActive = estActive;
}
public Boolean getInscriptionsOuvertes() {
return inscriptionsOuvertes != null ? inscriptionsOuvertes : false;
}
public void setInscriptionsOuvertes(Boolean inscriptionsOuvertes) {
this.inscriptionsOuvertes = inscriptionsOuvertes;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Saison saison = (Saison) o;
return Objects.equals(id, saison.id);
}
@@ -35,12 +35,21 @@ public class PublicInscriptionController {
}
@GetMapping("/demarrer")
public String showDemarrer() {
public String showDemarrer(Model model) {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
model.addAttribute("inscriptionsFermees", true);
}
return "public/demarrer";
}
@PostMapping("/demarrer")
public String processDemarrer(@RequestParam("cf-turnstile-response") String captchaResponse, RedirectAttributes redirectAttributes) {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
return "redirect:/inscription-public/demarrer";
}
// En vrai, captchaResponse dépend du fournisseur. Cloudflare Turnstile = cf-turnstile-response
if (!captchaValidationService.validateCaptcha(captchaResponse)) {
redirectAttributes.addFlashAttribute("error", "Validation CAPTCHA échouée. Veuillez réessayer.");
@@ -56,7 +65,13 @@ public class PublicInscriptionController {
}
@GetMapping("/formulaire")
public String showFormulaire(@RequestParam("token") String tokenUuid, Model model) {
public String showFormulaire(@RequestParam("token") String tokenUuid, Model model, RedirectAttributes redirectAttributes) {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
return "redirect:/inscription-public/demarrer";
}
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
if (token == null || !token.isValide()) {
return "public/lien-expire";
@@ -77,8 +92,8 @@ public class PublicInscriptionController {
}
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions sont actuellement fermées.");
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
return "redirect:/inscription-public/demarrer";
}
@@ -58,4 +58,13 @@ public class SaisonController {
return "redirect:/saisons";
}
@PostMapping("/{id}/toggle-inscriptions")
public String toggleInscriptions(@PathVariable Long id) {
Saison saison = saisonRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Saison introuvable : " + id));
saison.setInscriptionsOuvertes(!saison.getInscriptionsOuvertes());
saisonRepository.save(saison);
return "redirect:/saisons";
}
}
@@ -30,12 +30,13 @@
<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 de la Saison</th>
<th class="py-3 px-6 font-medium text-center">Statut</th>
<th class="py-3 px-6 font-medium text-center">Inscriptions Publiques</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(saisons)}">
<td colspan="3" class="py-8 text-center text-gray-500">Aucune saison configurée.</td>
<td colspan="4" class="py-8 text-center text-gray-500">Aucune saison configurée.</td>
</tr>
<tr th:each="saison : ${saisons}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900" th:text="${saison.nom}">Saison 2024-2025</td>
@@ -47,6 +48,14 @@
Clôturée
</span>
</td>
<td class="py-4 px-6 text-center">
<form th:if="${saison.estActive}" th:action="@{/saisons/{id}/toggle-inscriptions(id=${saison.id})}" method="post">
<button type="submit" th:class="${saison.inscriptionsOuvertes ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'} + ' px-3 py-1 rounded-full text-xs font-medium hover:opacity-80 transition-opacity'" title="Cliquez pour ouvrir/fermer">
<span th:text="${saison.inscriptionsOuvertes ? 'Ouvertes' : 'Fermées'}"></span>
</button>
</form>
<span th:unless="${saison.estActive}" class="text-gray-400 text-xs">-</span>
</td>
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
<form th:if="${!saison.estActive}" th:action="@{/saisons/{id}/activer(id=${saison.id})}" method="post">
<button type="submit" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Définir Active</button>
@@ -1,30 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>AS Talange - Pré-inscription</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">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<style> body { font-family: 'Inter', sans-serif; } </style>
<style>
body {
font-family: 'Inter', sans-serif;
}
</style>
</head>
<body class="bg-gray-50 flex items-center justify-center min-h-screen">
<div class="max-w-md w-full bg-white rounded-xl shadow-md p-8 border border-gray-100 text-center">
<img src="/images/logo.png" alt="AS Talange" class="h-24 mx-auto mb-6 opacity-80" onerror="this.style.display='none'">
<img src="/images/logo.png" alt="AS Talange" class="h-32 w-auto mx-auto mb-6 object-contain drop-shadow-sm"
onerror="this.style.display='none'">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Inscription au club</h1>
<p class="text-gray-600 mb-8">Veuillez valider le captcha pour accéder au formulaire sécurisé.</p>
<p th:unless="${inscriptionsFermees}" class="text-gray-600 mb-8">Veuillez valider le captcha pour accéder au formulaire sécurisé.</p>
<div th:if="${error}" class="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm" th:text="${error}"></div>
<div th:if="${param.error}" class="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm">Les inscriptions sont actuellement fermées.</div>
<div th:if="${error != null and (inscriptionsFermees == null or !inscriptionsFermees)}" class="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm" th:text="${error}"></div>
<form th:action="@{/inscription-public/demarrer}" method="post" class="flex flex-col items-center">
<div th:if="${inscriptionsFermees}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm font-medium">
Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.
</div>
<form th:unless="${inscriptionsFermees}" th:action="@{/inscription-public/demarrer}" method="post" class="flex flex-col items-center">
<!-- Remplacer par la vraie clé de site Cloudflare Turnstile -->
<div class="cf-turnstile mb-6" data-sitekey="1x00000000000000000000AA"></div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors">
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors">
Accéder au formulaire
</button>
</form>
</div>
</body>
</html>
@@ -9,7 +9,8 @@
</head>
<body class="bg-gray-50 flex items-center justify-center min-h-screen py-12">
<div class="max-w-xl w-full bg-white rounded-xl shadow-md p-8 border border-gray-100">
<h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">Formulaire de pré-inscription</h2>
<img src="/images/logo.png" alt="AS Talange" class="h-24 w-auto mx-auto mb-4 object-contain drop-shadow-sm" onerror="this.style.display='none'">
<h2 class="text-2xl font-bold text-gray-900 mb-2 text-center">Formulaire de pré-inscription</h2>
<p class="text-sm text-gray-500 mb-8 text-center">Veuillez remplir les informations concernant le futur licencié.</p>
<form th:action="@{/inscription-public/formulaire}" th:object="${preInscription}" method="post" class="space-y-6">