feat: gestion type de demande, calcul dynamique du tarif et pre-selection equipement (maillot) pour inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m28s

This commit is contained in:
2026-06-24 23:56:46 +02:00
parent 2df3e792f1
commit 5f2bf61829
13 changed files with 292 additions and 19 deletions
@@ -82,21 +82,8 @@ public class LicenceController {
licence = licenceRepository.save(licence);
// Auto-initialize dotations for all configured equipments of the category
List<CategorieEquipement> catEquipements = categorie.getCategorieEquipements();
if (catEquipements != null) {
for (CategorieEquipement ce : catEquipements) {
Dotation dotation = new Dotation();
dotation.setLicence(licence);
dotation.setEquipement(ce.getEquipement());
dotation.setTaille("");
dotation.setFlocage(adherent.getNom().toUpperCase() + " " + adherent.getPrenom());
dotation.setNumero("");
dotation.setFourni(false);
dotation.setChoisi(Boolean.TRUE.equals(ce.getObligatoire()));
dotationRepository.save(dotation);
}
}
// Auto-initialize dotations via CategorieService to ensure consistency
categorieService.syncDotationsForLicence(licence);
return "redirect:/adherents/" + id + "/edit";
}
@@ -6,6 +6,8 @@ import com.astalange.core.entity.Saison;
import com.astalange.core.repository.PreInscriptionRepository;
import com.astalange.core.repository.TokenPreInscriptionRepository;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.entity.Categorie;
import com.astalange.core.service.CaptchaValidationService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@@ -14,7 +16,10 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.beans.factory.annotation.Value;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.UUID;
import java.util.List;
import java.math.BigDecimal;
@Controller
@RequestMapping("/inscription-public")
@@ -24,15 +29,18 @@ public class PublicInscriptionController {
private final TokenPreInscriptionRepository tokenRepository;
private final PreInscriptionRepository preInscriptionRepository;
private final SaisonRepository saisonRepository;
private final CategorieRepository categorieRepository;
public PublicInscriptionController(CaptchaValidationService captchaValidationService,
TokenPreInscriptionRepository tokenRepository,
PreInscriptionRepository preInscriptionRepository,
SaisonRepository saisonRepository) {
SaisonRepository saisonRepository,
CategorieRepository categorieRepository) {
this.captchaValidationService = captchaValidationService;
this.tokenRepository = tokenRepository;
this.preInscriptionRepository = preInscriptionRepository;
this.saisonRepository = saisonRepository;
this.categorieRepository = categorieRepository;
}
@Value("${captcha.sitekey:1x00000000000000000000AA}")
@@ -117,9 +125,69 @@ public class PublicInscriptionController {
token.setEstUtilise(true);
tokenRepository.save(token);
if ("NOUVELLE".equals(preInscription.getTypeDemande())) {
String tarifStr = "210 €"; // Default
if (preInscription.getDateNaissance() != null) {
int annee = preInscription.getDateNaissance().getYear();
List<Categorie> categories = categorieRepository.findBySaisonAndAnnee(activeSaison, annee);
if (!categories.isEmpty()) {
Categorie cat = categories.get(0);
if (cat.getTarifBase() != null) {
BigDecimal prixMaillot = BigDecimal.ZERO;
if (cat.isU11OrBelow()) {
prixMaillot = cat.getEquipementPrixByNom("Maillot");
}
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
tarifStr = tarifTotal.intValue() + "";
}
}
}
redirectAttributes.addFlashAttribute("prenom", preInscription.getPrenom());
redirectAttributes.addFlashAttribute("tarif", tarifStr);
redirectAttributes.addFlashAttribute("documents", java.util.Arrays.asList(
"Photo didentité",
"Carte didentité ou passeport de lenfant",
"Livret de famille (si pas de pièce didentité)",
"Adresse e-mail du représentant légal",
"Pour les enfants nés à l’étranger : passeport + justificatif de domicile des parents"
));
redirectAttributes.addFlashAttribute("isNouvelle", true);
}
return "redirect:/inscription-public/succes";
}
@GetMapping("/api/tarif")
@ResponseBody
public String getTarif(@RequestParam(value = "dateNaissance", required = false) String dateNaissanceStr) {
if (dateNaissanceStr == null || dateNaissanceStr.isEmpty()) {
return "210 €";
}
try {
LocalDate dateNaissance = LocalDate.parse(dateNaissanceStr);
int annee = dateNaissance.getYear();
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null) return "210 €";
List<Categorie> categories = categorieRepository.findBySaisonAndAnnee(activeSaison, annee);
if (categories.isEmpty()) return "210 €";
Categorie cat = categories.get(0);
if (cat.getTarifBase() != null) {
BigDecimal prixMaillot = BigDecimal.ZERO;
if (cat.isU11OrBelow()) {
prixMaillot = cat.getEquipementPrixByNom("Maillot");
}
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
return tarifTotal.intValue() + "";
}
} catch (Exception e) {
// Return default on error
}
return "210 €";
}
@GetMapping("/succes")
public String showSucces() {
return "public/succes";
@@ -148,6 +148,24 @@
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<!-- Type de demande (Read-only) -->
<div>
<label for="typeDemande" class="block text-sm font-medium text-gray-700 mb-1">Type de demande (Pré-inscription)</label>
<input type="text" id="typeDemande" th:field="*{typeDemande}" readonly
class="w-full border border-gray-200 bg-gray-50 rounded-lg px-4 py-2 text-sm text-gray-500 outline-none cursor-not-allowed">
</div>
<!-- Ancien club -->
<div>
<label for="ancienClub" class="block text-sm font-medium text-gray-700 mb-1">Ancien club</label>
<input type="text" id="ancienClub" th:field="*{ancienClub}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
th:classappend="${#fields.hasErrors('ancienClub')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('ancienClub')}" th:errors="*{ancienClub}" class="mt-1 text-xs text-red-600"></p>
</div>
</div>
<!-- Removed Adresse -->
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
@@ -549,7 +567,18 @@
form.action = '/adherents/' + adherentId + '/licences';
document.getElementById('numeroLicenceInput').value = '';
document.getElementById('etatSelect').value = 'Brouillon';
document.getElementById('typeDemandeSelect').value = '';
// Pré-remplissage du type de demande
const adherentTypeDemande = document.getElementById('typeDemande') ? document.getElementById('typeDemande').value : '';
const typeDemandeSelect = document.getElementById('typeDemandeSelect');
if (adherentTypeDemande === 'NOUVELLE') {
typeDemandeSelect.value = 'Nouvelle demande';
} else if (adherentTypeDemande === 'RENOUVELLEMENT') {
typeDemandeSelect.value = 'Renouvellement';
} else {
typeDemandeSelect.value = '';
}
document.getElementById('typeLicenceSelect').value = '';
// Pré-sélection de la catégorie selon l'année de naissance
@@ -14,9 +14,50 @@
<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>
<div class="mb-6 p-4 bg-blue-50 border-l-4 border-blue-500 text-blue-700 text-sm rounded">
<h3 class="font-bold mb-1">Information sur le type de demande</h3>
<ul class="list-disc pl-5 space-y-1">
<li><strong>Nouvelle licence :</strong> Concerne les personnes qui n'étaient pas inscrites au club la saison précédente.</li>
<li><strong>Renouvellement :</strong> Réservé aux personnes déjà inscrites au club la saison précédente.</li>
</ul>
</div>
<form th:action="@{/inscription-public/formulaire}" th:object="${preInscription}" method="post" class="space-y-6">
<input type="hidden" name="token" th:value="${tokenUuid}" />
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700">Type de demande <span class="text-red-500">*</span></label>
<div class="flex items-center space-x-6">
<label class="flex items-center">
<input type="radio" th:field="*{typeDemande}" value="NOUVELLE" required class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">Nouvelle licence</span>
</label>
<label class="flex items-center">
<input type="radio" th:field="*{typeDemande}" value="RENOUVELLEMENT" required class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">Renouvellement</span>
</label>
</div>
</div>
<div id="nouvelleLicenceBlock" class="hidden space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Ancien club</label>
<input type="text" th:field="*{ancienClub}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Nom du club (laisser vide si aucun)">
</div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-lg">
<h4 class="font-bold text-yellow-800 mb-2">Documents requis</h4>
<p class="text-sm text-yellow-700 mb-2">Veuillez préparer les documents suivants pour finaliser la licence :</p>
<ul class="list-disc pl-5 text-sm text-yellow-700 space-y-1">
<li>Photo didentité</li>
<li>Carte didentité ou passeport de lenfant</li>
<li>Livret de famille (si pas de pièce didentité)</li>
<li>Adresse e-mail du représentant légal</li>
<li>Pour les enfants nés à l’étranger : passeport + justificatif de domicile des parents</li>
</ul>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label>
@@ -88,6 +129,25 @@
</div>
<script>
const typeDemandeRadios = document.querySelectorAll('input[name="typeDemande"]');
const nouvelleLicenceBlock = document.getElementById('nouvelleLicenceBlock');
typeDemandeRadios.forEach(radio => {
radio.addEventListener('change', function() {
if (this.value === 'NOUVELLE') {
nouvelleLicenceBlock.classList.remove('hidden');
} else {
nouvelleLicenceBlock.classList.add('hidden');
}
});
});
// Trigger on load if already checked (e.g. going back or validation error)
const checkedRadio = document.querySelector('input[name="typeDemande"]:checked');
if (checkedRadio && checkedRadio.value === 'NOUVELLE') {
nouvelleLicenceBlock.classList.remove('hidden');
}
document.getElementById('dateNaissance').addEventListener('change', function() {
const dateInput = this.value;
const repBlock = document.getElementById('representantBlock');
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8">
<title>Inscription réussie</title>
@@ -16,8 +17,56 @@
</svg>
</div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">Pré-inscription envoyée !</h1>
<p class="text-gray-600 mb-2">Votre dossier a bien été transmis au secrétariat de l'AS Talange.</p>
<p class="text-gray-600 mb-2" th:text="'Votre dossier pour ' + ${prenom != null ? prenom : 'le futur licencié'} + ' a bien été transmis au secrétariat de l\'AS Talange.'">Votre dossier a bien été transmis au secrétariat de l'AS Talange.</p>
<p class="text-sm text-gray-500">Un responsable va traiter votre demande prochainement. Vous pouvez fermer cette page.</p>
<div th:if="${isNouvelle}" class="mt-8 text-left">
<div id="ticket-documents" class="bg-yellow-50 border-2 border-dashed border-yellow-300 p-6 rounded-lg relative overflow-hidden shadow-sm">
<!-- Decorative elements for ticket look -->
<div class="absolute -left-3 top-1/2 w-6 h-6 bg-white rounded-full border-r-2 border-yellow-300 transform -translate-y-1/2"></div>
<div class="absolute -right-3 top-1/2 w-6 h-6 bg-white rounded-full border-l-2 border-yellow-300 transform -translate-y-1/2"></div>
<h2 class="text-lg font-bold text-yellow-800 mb-4 text-center border-b-2 border-yellow-200 pb-2 border-dashed uppercase tracking-wider">Mémo Inscription</h2>
<div class="mb-4">
<p class="text-sm font-semibold text-yellow-900">Tarif Licence : <span class="text-lg" th:text="${tarif}">210 €</span></p>
</div>
<h3 class="font-semibold text-yellow-900 mb-2 text-sm uppercase tracking-wide">Documents à fournir :</h3>
<ul class="list-none space-y-2 mb-3">
<li th:each="doc : ${documents}" class="flex items-start">
<svg class="w-4 h-4 text-yellow-500 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>
<span class="text-sm text-yellow-800" th:text="${doc}">Document</span>
</li>
</ul>
<p class="text-xs text-yellow-800 italic text-center mb-2">Ces documents sont à fournir au secrétariat du club pour finaliser l'inscription.</p>
<div class="mt-4 pt-2 border-t-2 border-yellow-200 border-dashed text-center">
<p class="text-xs text-yellow-600 font-medium">As Talange</p>
</div>
</div>
<div class="mt-4 text-center">
<button type="button" onclick="captureTicket()" class="bg-gray-800 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg shadow transition-colors inline-flex items-center text-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
Enregistrer cette liste (Image)
</button>
</div>
</div>
</div>
<script th:if="${isNouvelle}">
function captureTicket() {
const ticket = document.getElementById('ticket-documents');
html2canvas(ticket, { scale: 2 }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'ASTalange-Documents.png';
link.href = imgData;
link.click();
});
}
</script>
</body>
</html>