Initial commit - nettoyage et initialisation

This commit is contained in:
2026-05-19 22:01:18 +02:00
commit ee85d38c43
51 changed files with 2333 additions and 0 deletions
@@ -0,0 +1,328 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Nouvel Adhérent - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></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; }
.hidden-block { display: none; }
</style>
</head>
<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="#" 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>
</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>
<!-- Main Content -->
<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" th:text="${adherent.id == null ? 'Ajouter un adhérent' : 'Modifier l''adhérent'}">Ajouter un adhérent</h2>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6">
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<form th:action="@{/adherents}" th:object="${adherent}" method="post" class="space-y-6">
<input type="hidden" th:field="*{id}" />
<div th:if="${#fields.hasErrors('global')}" class="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg text-sm mb-6">
<p th:each="err : ${#fields.errors('global')}" th:text="${err}"></p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Nom -->
<div>
<label for="nom" class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" 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 focus:border-blue-500 outline-none transition-colors"
th:classappend="${#fields.hasErrors('nom')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" class="mt-1 text-xs text-red-600"></p>
</div>
<!-- Prénom -->
<div>
<label for="prenom" class="block text-sm font-medium text-gray-700 mb-1">Prénom <span class="text-red-500">*</span></label>
<input type="text" id="prenom" 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 focus:border-blue-500 outline-none transition-colors"
th:classappend="${#fields.hasErrors('prenom')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('prenom')}" th:errors="*{prenom}" class="mt-1 text-xs text-red-600"></p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Date de Naissance -->
<div>
<label for="dateNaissance" class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
<input type="date" id="dateNaissance" th:field="*{dateNaissance}" required
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('dateNaissance')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('dateNaissance')}" th:errors="*{dateNaissance}" class="mt-1 text-xs text-red-600"></p>
</div>
<!-- Téléphone -->
<div>
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
<input type="tel" id="telephone" 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 focus:border-blue-500 outline-none transition-colors"
th:classappend="${#fields.hasErrors('telephone')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('telephone')}" th:errors="*{telephone}" class="mt-1 text-xs text-red-600"></p>
</div>
</div>
<!-- Représentant Légal (Dynamique) -->
<div id="representantBlock" class="hidden-block bg-blue-50 p-4 rounded-lg border border-blue-100">
<label for="representantLegal" class="block text-sm font-medium text-blue-800 mb-1">
Représentant légal <span class="text-red-500">*</span>
</label>
<p class="text-xs text-blue-600 mb-2">Obligatoire car l'adhérent a moins de 18 ans.</p>
<input type="text" id="representantLegal" th:field="*{representantLegal}"
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('representantLegal')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('representantLegal')}" th:errors="*{representantLegal}" class="mt-1 text-xs text-red-600"></p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="email" id="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 focus:border-blue-500 outline-none transition-colors"
th:classappend="${#fields.hasErrors('email')} ? 'border-red-500' : ''">
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="mt-1 text-xs text-red-600"></p>
</div>
<!-- Résident Talange -->
<div class="flex items-center space-x-3 mt-7">
<input type="checkbox" id="residentTalange" th:field="*{residentTalange}" class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<label for="residentTalange" class="text-sm font-medium text-gray-700">
Résident de la commune de Talange
</label>
</div>
</div>
<!-- Adresse -->
<div>
<label for="adresse" class="block text-sm font-medium text-gray-700 mb-1">Adresse postale</label>
<textarea id="adresse" th:field="*{adresse}" rows="3"
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('adresse')} ? 'border-red-500' : ''"></textarea>
<p th:if="${#fields.hasErrors('adresse')}" th:errors="*{adresse}" class="mt-1 text-xs text-red-600"></p>
</div>
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
<a th:href="@{/adherents}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">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 transition-colors">Enregistrer</button>
</div>
</form>
</div>
<!-- Licences Section (Only visible for existing adherents) -->
<div th:if="${adherent.id != null}" class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Licences de l'adhérent</h3>
<button onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
+ Ajouter une Licence
</button>
</div>
<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-4 font-medium">Saison</th>
<th class="py-3 px-4 font-medium">Catégorie</th>
<th class="py-3 px-4 font-medium">N° Licence</th>
<th class="py-3 px-4 font-medium">État</th>
<th class="py-3 px-4 font-medium">Reste à Payer</th>
<th class="py-3 px-4 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr th:if="${#lists.isEmpty(licences)}">
<td colspan="6" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
</tr>
<th:block th:each="lic : ${licences}">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td>
<td class="py-4 px-4 text-gray-600" th:text="${lic.categorie.nom}">U15</td>
<td class="py-4 px-4 text-gray-600 font-mono text-xs" th:text="${lic.numeroLicence != null and !lic.numeroLicence.isEmpty() ? lic.numeroLicence : '-'}">-</td>
<td class="py-4 px-4">
<span class="px-2 py-1 text-xs font-semibold rounded-full"
th:classappend="${lic.etat == 'Validée' ? 'bg-blue-100 text-blue-800' : (lic.etat == 'Payée' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800')}"
th:text="${lic.etat}">Brouillon</span>
</td>
<td class="py-4 px-4 text-gray-600 font-medium" th:text="${lic.getResteAPayer() + ' €'}">100.00 €</td>
<td class="py-4 px-4 text-right">
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'"
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">Payer</button>
</td>
</tr>
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
<td colspan="5" class="py-2 px-4">
<div class="text-xs text-gray-500 mb-1 font-medium uppercase tracking-wider">Historique des paiements</div>
<div class="flex flex-wrap gap-2">
<span th:each="paiement : ${lic.paiements}" class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-white border border-gray-200 text-gray-700">
<span th:text="${paiement.montant + ' €'}">50 €</span>
<span class="mx-1 text-gray-300">|</span>
<span th:text="${paiement.modePaiement.nom}">Chèque</span>
<span class="mx-1 text-gray-300">|</span>
<span th:text="${#temporals.format(paiement.datePaiement, 'dd/MM/yyyy')}">01/01/2026</span>
</span>
</div>
</td>
</tr>
</th:block>
</tbody>
</table>
</div>
</div>
</main>
<!-- Licence Modal -->
<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">
<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">
<option value="">Sélectionnez une catégorie...</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom} + ' (' + ${cat.tarifBase} + ' €)'"></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">
</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">
</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">
<option value="Brouillon">Brouillon</option>
<option value="Validée">Validée</option>
<option value="Payée">Payée</option>
</select>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2">
<button type="button" onclick="closeLicenceModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
<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>
</div>
<!-- Paiement Modal -->
<div id="paiementModal" 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">Enregistrer un Paiement</h3>
<form id="paiementForm" method="post" class="space-y-4 text-left">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="hidden" name="adherentId" th:value="${adherent.id}">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Montant (€)</label>
<input type="number" step="0.01" id="paiementMontant" name="montant" 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">Date</label>
<input type="date" id="paiementDate" name="datePaiement" 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">Mode de paiement</label>
<select name="modePaiementId" 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 mode...</option>
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option>
</select>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2">
<button type="button" onclick="closePaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700">Payer</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openLicenceModal() {
document.getElementById('licenceModal').classList.remove('hidden');
}
function closeLicenceModal() {
document.getElementById('licenceModal').classList.add('hidden');
}
function openPaiementModal(licenceId, maxAmount) {
document.getElementById('paiementModal').classList.remove('hidden');
document.getElementById('paiementForm').action = '/licences/' + licenceId + '/paiements';
document.getElementById('paiementMontant').value = maxAmount;
document.getElementById('paiementMontant').max = maxAmount;
document.getElementById('paiementDate').valueAsDate = new Date();
}
function closePaiementModal() {
document.getElementById('paiementModal').classList.add('hidden');
}
document.addEventListener("DOMContentLoaded", function() {
const dateInput = document.getElementById('dateNaissance');
const repBlock = document.getElementById('representantBlock');
function calculateAge(birthday) {
const ageDifMs = Date.now() - birthday.getTime();
const ageDate = new Date(ageDifMs);
return Math.abs(ageDate.getUTCFullYear() - 1970);
}
function updateRepresentantBlock() {
if (dateInput.value) {
const dob = new Date(dateInput.value);
const age = calculateAge(dob);
if (age < 18) {
repBlock.classList.remove('hidden-block');
} else {
repBlock.classList.add('hidden-block');
}
} else {
repBlock.classList.add('hidden-block');
}
}
dateInput.addEventListener('change', updateRepresentantBlock);
// Initial check in case it's a validation error reload
updateRepresentantBlock();
// But if there's already an error on representantLegal, ensure it is shown
const hasRepError = document.querySelector('p[th\\:errors="*{representantLegal}"]');
if (hasRepError || document.getElementById('representantLegal').classList.contains('border-red-500')) {
repBlock.classList.remove('hidden-block');
}
});
</script>
</body>
</html>