feat: implémentation de la gestion des saisons (UI, service, refactoring sidebar)

This commit is contained in:
2026-05-29 22:35:33 +02:00
parent dd31d73fed
commit a429e67133
27 changed files with 591 additions and 185 deletions
@@ -14,26 +14,7 @@
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
<div class="h-16 flex items-center px-6 border-b border-gray-200">
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
</div>
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Tableau de bord</a>
<a href="/adherents" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Adhérents</a>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Équipements</a>
</nav>
<div class="p-4 border-t border-gray-200">
<div class="text-sm font-medium text-gray-900 mb-1" th:text="${username != null ? username : 'Admin'}">Admin</div>
<form th:action="@{/logout}" method="post">
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
</form>
</div>
</aside>
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden">
@@ -191,7 +172,7 @@
th:data-tarif-exterieur="${lic.categorie.tarifExterieur}"
th:data-equipements-contrib="${lic.getPrixTotal().subtract(adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur)}"
th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}">
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td>
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison.nom}">2024-2025</td>
<td class="py-4 px-4 text-gray-600">
<span th:text="${lic.categorie.nom}">U15</span>
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
@@ -206,9 +187,20 @@
<td class="py-4 px-4 font-semibold cell-reste-payer"
th:classappend="${lic.getResteAPayer() == 0 ? 'text-green-600' : 'text-red-600'}"
th:text="${lic.getResteAPayer() + ' €'}">30.00 €</td>
<td class="py-4 px-4 text-right">
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'"
th:data-licence-id="${lic.id}" class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg btn-payer">Payer</button>
<td class="py-4 px-4 text-right flex justify-end space-x-2">
<button type="button"
th:data-licence-id="${lic.id}"
th:data-categorie-id="${lic.categorie.id}"
th:data-numero-licence="${lic.numeroLicence}"
th:data-etat="${lic.etat}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'))"
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
<button th:if="${lic.getResteAPayer() > 0}"
type="button"
th:data-licence-id="${lic.id}"
th:data-reste-a-payer="${lic.getResteAPayer()}"
onclick="openPaiementModal(this.getAttribute('data-licence-id'), this.getAttribute('data-reste-a-payer'))"
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg btn-payer">Payer</button>
</td>
</tr>
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
@@ -323,26 +315,29 @@
<div id="licenceModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-xl bg-white">
<div class="mt-3 text-center">
<h3 class="text-lg leading-6 font-semibold text-gray-900 mb-4">Nouvelle Licence</h3>
<form th:if="${adherent.id != null}" th:action="@{/adherents/{id}/licences(id=${adherent.id})}" method="post" class="space-y-4 text-left">
<h3 id="licenceModalTitle" class="text-lg leading-6 font-semibold text-gray-900 mb-4">Nouvelle Licence</h3>
<form id="licenceForm" th:if="${adherent.id != null}" th:action="@{/adherents/{id}/licences(id=${adherent.id})}" method="post" class="space-y-4 text-left">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
<select name="categorieId" 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">
<select id="categorieSelect" name="categorieId" 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} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
<option th:each="cat : ${categories}" th:value="${cat.id}"
th:data-annee-min="${cat.anneeMin}"
th:data-annee-max="${cat.anneeMax}"
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
<input type="text" name="numeroLicence" placeholder="Ex: FFF-123456" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<input id="numeroLicenceInput" type="text" name="numeroLicence" placeholder="Ex: FFF-123456" 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">Saison</label>
<input type="text" name="saison" required value="2024-2025" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<input id="saisonInput" type="text" name="saison" value="Saison Actuelle" 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>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">État</label>
<select name="etat" 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">
<select id="etatSelect" name="etat" 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="Brouillon">Brouillon</option>
<option value="Validée">Validée</option>
<option value="Payée">Payée</option>
@@ -389,9 +384,47 @@
</div>
</div>
<script>
function openLicenceModal() {
<script th:inline="javascript">
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon') {
document.getElementById('licenceModal').classList.remove('hidden');
const form = document.getElementById('licenceForm');
const title = document.getElementById('licenceModalTitle');
const adherentId = [[${adherent.id}]];
if (licenceId) {
title.textContent = 'Modifier la Licence';
form.action = '/adherents/' + adherentId + '/licences/' + licenceId + '/update';
document.getElementById('categorieSelect').value = categorieId;
document.getElementById('numeroLicenceInput').value = numeroLicence;
document.getElementById('etatSelect').value = etat;
} else {
title.textContent = 'Nouvelle Licence';
form.action = '/adherents/' + adherentId + '/licences';
document.getElementById('numeroLicenceInput').value = '';
document.getElementById('etatSelect').value = 'Brouillon';
// Pré-sélection de la catégorie selon l'année de naissance
const dateNaissanceStr = document.getElementById('dateNaissance').value;
const catSelect = document.getElementById('categorieSelect');
let foundCat = false;
if (dateNaissanceStr) {
const birthYear = new Date(dateNaissanceStr).getFullYear();
Array.from(catSelect.options).forEach(opt => {
const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max'));
if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) {
catSelect.value = opt.value;
foundCat = true;
}
});
}
if (!foundCat) {
catSelect.value = '';
}
}
}
function closeLicenceModal() {
document.getElementById('licenceModal').classList.add('hidden');
@@ -13,26 +13,7 @@
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
<div class="h-16 flex items-center px-6 border-b border-gray-200">
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
</div>
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Tableau de bord</a>
<a href="/adherents" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Adhérents</a>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Équipements</a>
</nav>
<div class="p-4 border-t border-gray-200">
<div class="text-sm font-medium text-gray-900 mb-1" th:text="${username != null ? username : 'Admin'}">Admin</div>
<form th:action="@{/logout}" method="post">
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
</form>
</div>
</aside>
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden">
@@ -55,7 +36,12 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<input type="text" placeholder="Rechercher un adhérent..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<form th:action="@{/adherents}" method="get" class="flex items-center space-x-2">
<input type="text" name="query" th:value="${searchQuery}" placeholder="Nom, Prénom ou N° Licence..."
class="border border-gray-300 rounded-lg px-4 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button type="submit" class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 border border-gray-300 transition-colors">Chercher</button>
<a th:if="${searchQuery != null and !searchQuery.isEmpty()}" th:href="@{/adherents}" class="text-sm text-gray-500 hover:text-gray-700 underline underline-offset-2 ml-2">Effacer</a>
</form>
</div>
<table class="w-full text-left border-collapse">
<thead>