feat: gestion des utilisateurs, rôles et équipes

- Ajout de la section de gestion des utilisateurs avec rôles (Admin, Trésorerie, Agent de Saisie).
- Restriction des accès par rôle (Admin a tout, Trésorerie a adhérents/paiements, Agent de saisie a adhérents/planning).
- Flux de modification obligatoire de mot de passe temporaire au premier login (par défaut AsTalange123).
- Intégration de la configuration des équipes du club.
This commit is contained in:
2026-05-30 23:11:12 +02:00
parent 7e9504ffe0
commit 8a15139efb
29 changed files with 948 additions and 35 deletions
@@ -174,7 +174,7 @@
th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}">
<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 th:text="${lic.categorie.nom + (lic.equipe != null ? ' (' + lic.equipe.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>
</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>
@@ -193,7 +193,8 @@
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'))"
th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-equipe-id'))"
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"
@@ -330,6 +331,15 @@
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">Équipe</label>
<select id="licenceEquipe" name="equipeId" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<th:block th:fragment="equipe-options">
<option value="">Aucune équipe</option>
<option th:each="team : ${equipes}" th:value="${team.id}" th:text="${team.nom}"></option>
</th:block>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
<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">
@@ -388,18 +398,33 @@
</div>
<script th:inline="javascript">
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon') {
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', equipeId = '') {
document.getElementById('licenceModal').classList.remove('hidden');
const form = document.getElementById('licenceForm');
const title = document.getElementById('licenceModalTitle');
const adherentId = [[${adherent.id}]];
const catSelect = document.getElementById('categorieSelect');
const equipeSelect = document.getElementById('licenceEquipe');
// Clean up team options first
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
if (licenceId) {
title.textContent = 'Modifier la Licence';
form.action = '/adherents/' + adherentId + '/licences/' + licenceId + '/update';
document.getElementById('categorieSelect').value = categorieId;
catSelect.value = categorieId;
document.getElementById('numeroLicenceInput').value = numeroLicence;
document.getElementById('etatSelect').value = etat;
// Fetch and populate teams
if (categorieId) {
fetch('/categories/equipes?categorieId=' + categorieId)
.then(response => response.text())
.then(html => {
equipeSelect.innerHTML = html;
equipeSelect.value = equipeId;
});
}
} else {
title.textContent = 'Nouvelle Licence';
form.action = '/adherents/' + adherentId + '/licences';
@@ -408,8 +433,8 @@
// 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;
let preSelectedCatId = '';
if (dateNaissanceStr) {
const birthYear = new Date(dateNaissanceStr).getFullYear();
@@ -419,6 +444,7 @@
const max = parseInt(opt.getAttribute('data-annee-max'));
if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) {
catSelect.value = opt.value;
preSelectedCatId = opt.value;
foundCat = true;
}
});
@@ -427,6 +453,15 @@
if (!foundCat) {
catSelect.value = '';
}
// Fetch and populate teams for the pre-selected category
if (preSelectedCatId) {
fetch('/categories/equipes?categorieId=' + preSelectedCatId)
.then(response => response.text())
.then(html => {
equipeSelect.innerHTML = html;
});
}
}
}
function closeLicenceModal() {
@@ -448,6 +483,22 @@
const repBlock = document.getElementById('representantBlock');
const residentCheckbox = document.getElementById('residentTalange');
const catSelect = document.getElementById('categorieSelect');
if (catSelect) {
catSelect.addEventListener('change', function() {
const catId = this.value;
const equipeSelect = document.getElementById('licenceEquipe');
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
if (catId) {
fetch('/categories/equipes?categorieId=' + catId)
.then(response => response.text())
.then(html => {
equipeSelect.innerHTML = html;
});
}
});
}
function calculateAge(birthday) {
const ageDifMs = Date.now() - birthday.getTime();
const ageDate = new Date(ageDifMs);