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);
@@ -107,7 +107,8 @@
</div>
</td>
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.prenom}">Jean</td>
<td class="py-4 px-6 text-gray-600 font-medium" th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() : '-'}">-</td>
<td class="py-4 px-6 text-gray-600 font-medium"
th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() + (adherent.getLicenceActuelle().getEquipe() != null ? ' (' + adherent.getLicenceActuelle().getEquipe().getNom() + ')' : '') : '-'}">-</td>
<td class="py-4 px-6 text-center text-gray-600 font-mono text-xs"
th:text="${adherent.getLicenceActuelle() != null and adherent.getLicenceActuelle().numeroLicence != null and !adherent.getLicenceActuelle().numeroLicence.isEmpty() ? adherent.getLicenceActuelle().numeroLicence : '-'}">-</td>
<td class="py-4 px-6 text-gray-600" th:text="${adherent.email}">jean@example.com</td>
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Changement de mot de passe - AS Talange</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">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50 flex items-center justify-center min-h-screen">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md border border-gray-100">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Première connexion</h1>
<p class="text-gray-500">Pour des raisons de sécurité, veuillez modifier votre mot de passe temporaire.</p>
</div>
<div th:if="${error}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm" th:text="${error}">
Une erreur est survenue.
</div>
<form th:action="@{/change-password}" method="post" class="space-y-6">
<div>
<label for="newPassword" class="block text-sm font-medium text-gray-700 mb-1">Nouveau mot de passe</label>
<input type="password" id="newPassword" name="newPassword" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required autofocus>
<p class="text-xs text-gray-400 mt-1">Le mot de passe doit être différent de AsTalange123.</p>
</div>
<div>
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">Confirmer le nouveau mot de passe</label>
<input type="password" id="confirmPassword" name="confirmPassword" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200">
Enregistrer le mot de passe
</button>
</form>
</div>
</body>
</html>
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
</head>
@@ -11,14 +11,25 @@
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
<a href="/" th:classappend="${requestURI == '/' ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Tableau de bord</a>
<a href="/adherents" th:classappend="${requestURI != null and requestURI.startsWith('/adherents') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Adhérents</a>
<a href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Paiements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Planning</div>
<a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Entraînements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Saisons</a>
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Catégories</a>
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Modes de Paiement</a>
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipements</a>
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_TRESORERIE')" href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Paiements</a>
<th:block sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_AGENT_SAISIE')">
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Planning</div>
<a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Entraînements</a>
</th:block>
<th:block sec:authorize="hasRole('ROLE_ADMIN')">
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Saisons</a>
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Catégories</a>
<a href="/equipes" th:classappend="${requestURI != null and requestURI.startsWith('/equipes') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipes</a>
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Modes de Paiement</a>
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div>
<a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a>
</th:block>
</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>
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Paramétrage - Équipes</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">
<style> body { font-family: 'Inter', sans-serif; } </style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
<h2 class="text-lg font-semibold text-gray-800">Configuration des Équipes</h2>
</header>
<div class="flex-1 overflow-auto p-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left: Add Team Form Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 h-fit">
<h3 class="text-lg font-bold text-gray-900 mb-4">Créer une Équipe</h3>
<form th:action="@{/equipes}" method="post" class="space-y-4">
<div>
<label for="nom" class="block text-sm font-medium text-gray-700 mb-1">Nom de l'équipe</label>
<input type="text" id="nom" name="nom" placeholder="Ex: Équipe A, Équipe 1" 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">
</div>
<div>
<label for="categorie" class="block text-sm font-medium text-gray-700 mb-1">Catégorie associée</label>
<select id="categorie" 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 focus:border-blue-500 outline-none transition-colors">
<option value="">Choisir une catégorie...</option>
<option th:each="cat : ${categories}" th:value="${cat.id}"
th:text="${cat.nom + ' (' + cat.saison.nom + ')'}"></option>
</select>
</div>
<div class="pt-2">
<button type="submit" class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-semibold transition-colors shadow-sm">
+ Ajouter l'équipe
</button>
</div>
</form>
</div>
<!-- Right: Teams List Card -->
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="font-bold text-gray-900">Équipes Paramétrées</h3>
<span class="text-xs font-semibold text-gray-500 bg-gray-200 px-2.5 py-1 rounded-full"
th:text="${saisonActive != null ? 'Saison active : ' + saisonActive.nom : 'Toutes les Saisons'}">Saison Actuelle</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
<th class="py-3 px-6 font-medium">Nom de l'équipe</th>
<th class="py-3 px-6 font-medium">Catégorie</th>
<th class="py-3 px-6 font-medium">Saison</th>
<th class="py-3 px-6 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 text-sm">
<tr th:if="${#lists.isEmpty(equipes)}">
<td colspan="4" class="py-8 text-center text-gray-500">Aucune équipe configurée pour le moment. Usez du formulaire à gauche pour en créer une.</td>
</tr>
<tr th:each="team : ${equipes}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900" th:text="${team.nom}">Équipe 1</td>
<td class="py-4 px-6 text-gray-600">
<span class="bg-blue-50 text-blue-700 px-2.5 py-1 rounded-full text-xs font-medium" th:text="${team.categorie.nom}">U15</span>
</td>
<td class="py-4 px-6 text-gray-500" th:text="${team.saison.nom}">2024-2025</td>
<td class="py-4 px-6 text-right flex justify-end items-center">
<form th:action="@{/equipes/{id}/delete(id=${team.id})}" method="post" onsubmit="return confirm('Supprimer cette équipe ? Elle sera dissociée de toutes ses licences.');">
<button type="submit" class="text-red-500 hover:text-red-700 font-medium text-xs bg-red-50 hover:bg-red-100 px-2.5 py-1.5 rounded-lg transition-colors">
Supprimer
</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</body>
</html>
@@ -33,6 +33,15 @@
</select>
</div>
<!-- Équipe (Optionnelle) -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Équipe</label>
<select id="planningEquipe" name="equipe.id" 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="">Aucune équipe</option>
<option th:each="team : ${equipes}" th:value="${team.id}" th:text="${team.nom}" th:selected="${creneau.equipe != null && creneau.equipe.id == team.id}"></option>
</select>
</div>
<!-- Terrain -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Terrain <span class="text-red-500">*</span></label>
@@ -95,5 +104,24 @@
</div>
</div>
</main>
<script>
document.addEventListener("DOMContentLoaded", function() {
const catSelect = document.getElementsByName('categorie.id')[0];
if (catSelect) {
catSelect.addEventListener('change', function() {
const catId = this.value;
const equipeSelect = document.getElementById('planningEquipe');
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
if (catId) {
fetch('/categories/equipes?categorieId=' + catId)
.then(response => response.text())
.then(html => {
equipeSelect.innerHTML = html;
});
}
});
}
});
</script>
</body>
</html>
@@ -147,7 +147,7 @@
class="border-l-4 p-2 rounded shadow-sm flex flex-col justify-between overflow-hidden cursor-pointer hover:shadow transition-all relative z-10 m-0.5">
<div class="flex justify-between items-start">
<span class="font-bold text-xs" th:text="${c.categorie.nom}">U15</span>
<span class="font-bold text-xs" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U15</span>
<!-- Delete button or info -->
<div class="flex items-center space-x-1">
<a th:href="@{/planning/{id}/edit(id=${c.id})}" class="text-[10px] text-gray-500 hover:underline">Modifier</a>
@@ -192,7 +192,7 @@
<td colspan="6" class="py-8 text-center text-gray-500">Aucun entraînement planifié pour ce jour.</td>
</tr>
<tr th:each="c : ${creneaux}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-semibold text-gray-900" th:text="${c.categorie.nom}">U11</td>
<td class="py-4 px-6 font-semibold text-gray-900" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U11</td>
<td class="py-4 px-6 text-gray-600" th:text="${c.terrain.nom}">Terrain Synthétique</td>
<td class="py-4 px-6 text-gray-600">
<span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs" th:text="${c.zone.libelle}">Demi A</span>
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Ajouter un Utilisateur - AS Talange</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">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<!-- 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">Ajouter un Utilisateur</h2>
<a href="/utilisateurs" class="text-sm font-medium text-gray-600 hover:text-gray-900">
&larr; Retour à la liste
</a>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6 bg-gray-50/50 flex justify-center items-start">
<div class="w-full max-w-lg bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-4">
<div th:if="${error}" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
<span th:text="${error}"></span>
</div>
<form th:action="@{/utilisateurs/ajouter}" method="post" class="space-y-6">
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Nom d'utilisateur</label>
<input type="text" id="username" name="username" th:value="${username}" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required placeholder="Ex: JeanDupont" autofocus>
</div>
<div>
<label for="roleId" class="block text-sm font-medium text-gray-700 mb-1">Rôle</label>
<select id="roleId" name="roleId" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors bg-white" required>
<option value="">Sélectionnez un rôle</option>
<option th:each="r : ${roles}" th:value="${r.id}"
th:text="${r.name == 'ROLE_ADMIN' ? 'Administrateur' : (r.name == 'ROLE_TRESORERIE' ? 'Trésorerie' : (r.name == 'ROLE_AGENT_SAISIE' ? 'Agent de Saisie' : r.name))}">
</option>
</select>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Mot de passe temporaire</label>
<input type="text" id="password" name="password" th:value="${defaultPassword}" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required>
<p class="text-xs text-gray-400 mt-1">Le mot de passe par défaut est AsTalange123. L'utilisateur devra le modifier lors de sa première connexion.</p>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200">
Créer l'utilisateur
</button>
</form>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Gestion des Utilisateurs - AS Talange</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">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<!-- 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">Gestion des Utilisateurs</h2>
<a href="/utilisateurs/ajouter" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors text-sm">
Ajouter un utilisateur
</a>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6 bg-gray-50/50">
<!-- Success / Error messages -->
<div th:if="${success}" class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg mb-6 text-sm">
<span th:text="${success}"></span>
</div>
<div th:if="${error}" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
<span th:text="${error}"></span>
</div>
<!-- Users Table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50/50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
<th class="py-3 px-6 font-medium">Nom d'utilisateur</th>
<th class="py-3 px-6 font-medium">Rôle</th>
<th class="py-3 px-6 font-medium">Statut</th>
<th class="py-3 px-6 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 text-sm">
<tr th:each="user : ${users}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900" th:text="${user.username}">
Ucef
</td>
<td class="py-4 px-6 text-gray-600">
<span th:each="role : ${user.roles}"
th:text="${role.name == 'ROLE_ADMIN' ? 'Administrateur' : (role.name == 'ROLE_TRESORERIE' ? 'Trésorerie' : (role.name == 'ROLE_AGENT_SAISIE' ? 'Agent de Saisie' : role.name))}"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mr-1">
Admin
</span>
</td>
<td class="py-4 px-6">
<span th:if="${user.mustChangePassword}" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Mot de passe temporaire
</span>
<span th:unless="${user.mustChangePassword}" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Actif
</span>
</td>
<td class="py-4 px-6 text-right">
<!-- Don't show delete button for the current user -->
<form th:if="${#authentication.name != user.username}"
th:action="@{/utilisateurs/{id}/delete(id=${user.id})}"
method="post"
class="inline"
onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?');">
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">
Supprimer
</button>
</form>
<span th:if="${#authentication.name == user.username}" class="text-gray-400 italic text-xs">
Connecté
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>