Compare commits

14 Commits

Author SHA1 Message Date
ucef 0ec330e6a8 feat: add active user session tracking and visualization in admin dashboard
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 2m1s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m45s
2026-07-03 17:28:42 +02:00
ucef 66cd27995d fix: resolve HTMX pagination bug on adherents list 2026-07-03 17:07:52 +02:00
ucef 4870d61f86 fix(db): increase taille and numero length in dotation to 50
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m49s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 3m0s
2026-07-01 15:07:53 +02:00
ucef 6ca3d0b765 feat(ui): ajout d'un tooltip sur les noms d'équipements tronqués dans la vue dotation
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m57s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m45s
2026-07-01 10:36:40 +02:00
ucef ba268a33bc feat: add sexe to pre-inscription form and entity 2026-07-01 09:32:42 +02:00
ucef f641ccc519 feat: add search filters to categories and equipements lists
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m45s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m42s
2026-06-30 15:11:16 +02:00
ucef 6ea00db0dc fix: apply maillot price and default selection for all new registrations
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m46s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m38s
2026-06-30 14:42:59 +02:00
ucef 64ad4be7dd chore: release v1.4
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m45s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m44s
2026-06-30 13:19:43 +02:00
ucef d30e795325 fix: harmoniser le titre des documents à fournir sur la page de succès
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 4m18s
2026-06-30 13:16:24 +02:00
ucef e0388c7884 revert: remove viewport meta tag to restore previous mobile display
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m21s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 1m40s
2026-06-30 12:45:37 +02:00
ucef 7cf29f6106 Merge remote-tracking branch 'origin/main'
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 10m47s
2026-06-29 16:30:07 +02:00
ucef bef01e3a0e feat: add viewport meta tag to all HTML templates for responsive mobile display 2026-06-29 16:29:58 +02:00
ucef 9cf1d26018 feat: add viewport meta tag to all HTML templates for responsive mobile display
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m6s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m13s
2026-06-29 15:44:48 +02:00
ucef 94d672c0f6 feat: handle goalkeeper vs field player equipment differentiation
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-29 14:54:36 +02:00
25 changed files with 253 additions and 41 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId>
<version>1.4-SNAPSHOT</version>
<version>1.4</version>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId>
<version>1.4-SNAPSHOT</version>
<version>1.4</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -18,7 +18,7 @@ public class Dotation {
@JoinColumn(name = "equipement_id", nullable = false)
private Equipement equipement;
@Column(length = 10)
@Column(length = 50)
private String taille;
@Column(length = 50)
@@ -30,7 +30,7 @@ public class Dotation {
@Column(nullable = false)
private Boolean fourni = false;
@Column(length = 10)
@Column(length = 50)
private String numero;
@Column(nullable = false)
@@ -23,6 +23,9 @@ public class Equipement {
@Column(name = "gestion_sexe", nullable = false)
private boolean gestionSexe = false;
@Column(name = "type_public", nullable = false, length = 20)
private String typePublic = "TOUS"; // JOUEUR, GARDIEN, TOUS
@Column(name = "tailles_disponibles", length = 500)
private String taillesDisponibles;
@@ -51,6 +54,9 @@ public class Equipement {
public boolean isGestionSexe() { return gestionSexe; }
public void setGestionSexe(boolean gestionSexe) { this.gestionSexe = gestionSexe; }
public String getTypePublic() { return typePublic; }
public void setTypePublic(String typePublic) { this.typePublic = typePublic; }
public String getTaillesDisponibles() { return taillesDisponibles; }
public void setTaillesDisponibles(String taillesDisponibles) { this.taillesDisponibles = taillesDisponibles; }
@@ -18,6 +18,9 @@ public class PreInscription {
@Column(nullable = false, length = 100)
private String prenom;
@Column(nullable = false, length = 10)
private String sexe;
@Column(name = "date_naissance", nullable = false)
private LocalDate dateNaissance;
@@ -71,6 +74,9 @@ public class PreInscription {
public String getPrenom() { return prenom; }
public void setPrenom(String prenom) { this.prenom = prenom; }
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public LocalDate getDateNaissance() { return dateNaissance; }
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
@@ -86,12 +86,21 @@ public class CategorieService {
List<Dotation> currentDotations = licence.getDotations();
List<Dotation> toRemove = new ArrayList<>();
// Remove dotations for equipements no longer in the category
// Remove dotations for equipements no longer in the category or not matching public
for (Dotation d : currentDotations) {
boolean found = false;
for (CategorieEquipement ce : categorieEquipements) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
found = true;
String typePublic = ce.getEquipement().getTypePublic();
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
boolean matchPublic = "TOUS".equals(typePublic) ||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
if (matchPublic) {
found = true;
}
break;
}
}
@@ -103,6 +112,17 @@ public class CategorieService {
// Add missing dotations
for (CategorieEquipement ce : categorieEquipements) {
String typePublic = ce.getEquipement().getTypePublic();
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
boolean matchPublic = "TOUS".equals(typePublic) ||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
if (!matchPublic) {
continue;
}
boolean found = false;
for (Dotation d : currentDotations) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
@@ -118,14 +138,13 @@ public class CategorieService {
newDotation.setLicence(licence);
newDotation.setEquipement(ce.getEquipement());
boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom());
boolean isNouvelleAndU11 = false;
boolean isNouvelle = false;
if (licence.getTypeDemande() != null &&
(licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) &&
licence.getCategorie() != null &&
licence.getCategorie().isU11OrBelow()) {
isNouvelleAndU11 = true;
licence.getCategorie() != null) {
isNouvelle = true;
}
newDotation.setChoisi(ce.getObligatoire() || (isMaillot && isNouvelleAndU11)); // Checked if obligatoire or if it's a new registration for U11 or below
newDotation.setChoisi(ce.getObligatoire() || (isMaillot && isNouvelle)); // Checked if obligatoire or if it's a new registration
newDotation.setFourni(false);
newDotation.setTaille("");
newDotation.setNumero("");
@@ -47,8 +47,8 @@ public class PreInscriptionService {
adherent.setTypeDemande(pre.getTypeDemande());
adherent.setAncienClub(pre.getAncienClub());
adherent.setAncienneCategorie(pre.getAncienneCategorie());
adherent.setSexe(pre.getSexe());
// Par défaut
adherent.setSexe("MASCULIN");
adherent.setTypeMaillot("JOUEUR");
adherent.setResidentTalange(false);
@@ -0,0 +1 @@
ALTER TABLE equipement ADD COLUMN type_public VARCHAR(20) DEFAULT 'TOUS' NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE pre_inscription ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE dotation ALTER COLUMN taille TYPE VARCHAR(50);
ALTER TABLE dotation ALTER COLUMN numero TYPE VARCHAR(50);
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId>
<version>1.4-SNAPSHOT</version>
<version>1.4</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -10,6 +10,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@Configuration
@EnableWebSecurity
@@ -41,6 +44,10 @@ public class SecurityConfig {
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.sessionManagement(session -> session
.maximumSessions(100)
.sessionRegistry(sessionRegistry())
);
return http.build();
}
@@ -57,4 +64,14 @@ public class SecurityConfig {
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
@@ -177,21 +177,29 @@ public class AdherentController {
Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null);
if (existing != null) {
boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance());
boolean typeMaillotChanged = !existing.getTypeMaillot().equals(adherent.getTypeMaillot());
if (dateChanged) {
if (dateChanged || typeMaillotChanged) {
int newBirthYear = adherent.getDateNaissance().getYear();
List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId());
for (Licence licence : licences) {
Saison saison = licence.getSaison();
Categorie newCat = categorieRepository.findBySaison(saison).stream()
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.findFirst()
.orElse(null);
if (newCat != null) {
licence.setCategorie(newCat);
licence = licenceRepository.save(licence);
categorieService.syncDotationsForLicence(licence);
if (dateChanged) {
Saison saison = licence.getSaison();
Categorie newCat = categorieRepository.findBySaison(saison).stream()
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.findFirst()
.orElse(null);
if (newCat != null) {
licence.setCategorie(newCat);
licence = licenceRepository.save(licence);
}
}
// We need to update the entity's state before syncing dotations so it reads the new typeMaillot.
// We haven't updated 'existing' yet, so let's temporarily set it on the licence's adherent or wait.
// Actually, 'adherent' has the new typeMaillot.
licence.getAdherent().setTypeMaillot(adherent.getTypeMaillot());
categorieService.syncDotationsForLicence(licence);
}
}
@@ -133,9 +133,9 @@ public class PublicInscriptionController {
if (!categories.isEmpty()) {
Categorie cat = categories.get(0);
if (cat.getTarifBase() != null) {
BigDecimal prixMaillot = BigDecimal.ZERO;
if (cat.isU11OrBelow()) {
prixMaillot = cat.getEquipementPrixByNom("Maillot");
BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
if (prixMaillot == null) {
prixMaillot = BigDecimal.ZERO;
}
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
tarifStr = tarifTotal.intValue() + "";
@@ -175,9 +175,9 @@ public class PublicInscriptionController {
Categorie cat = categories.get(0);
if (cat.getTarifBase() != null) {
BigDecimal prixMaillot = BigDecimal.ZERO;
if (cat.isU11OrBelow()) {
prixMaillot = cat.getEquipementPrixByNom("Maillot");
BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
if (prixMaillot == null) {
prixMaillot = BigDecimal.ZERO;
}
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
return tarifTotal.intValue() + "";
@@ -0,0 +1,37 @@
package com.astalange.web.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import java.util.stream.Collectors;
@Controller
@RequestMapping("/admin/sessions")
public class SessionController {
private final SessionRegistry sessionRegistry;
public SessionController(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public String viewSessions(Model model) {
List<Object> principals = sessionRegistry.getAllPrincipals();
List<String> activeUsers = principals.stream()
.filter(principal -> principal instanceof UserDetails)
.map(principal -> ((UserDetails) principal).getUsername())
.collect(Collectors.toList());
model.addAttribute("activeUsers", activeUsers);
model.addAttribute("activeCount", activeUsers.size());
return "admin/sessions";
}
}
@@ -358,7 +358,7 @@
<div>
<div class="flex justify-between items-start mb-1 gap-1">
<span class="font-semibold text-xs text-gray-800 truncate" th:text="${dot.equipement.nom}">Maillot</span>
<span class="font-semibold text-xs text-gray-800 truncate" th:title="${dot.equipement.nom}" th:text="${dot.equipement.nom}">Maillot</span>
<span class="text-[9px] px-1.5 py-0.5 rounded-full font-bold flex-shrink-0"
th:classappend="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'bg-red-50 text-red-700 border border-red-100' : 'bg-gray-50 text-gray-600 border border-gray-100'}"
th:text="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
@@ -175,7 +175,7 @@
hx-target="#adherents-table-container"
hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${currentPage - 1}}|"
th:attr="hx-vals=|{&quot;page&quot;: ${currentPage - 1}}|"
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Précédent
</button>
@@ -190,7 +190,7 @@
hx-target="#adherents-table-container"
hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${pageNum}}|"
th:attr="hx-vals=|{&quot;page&quot;: ${pageNum}}|"
th:text="${pageNum + 1}"
class="px-3 py-1 rounded transition-colors"
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
@@ -205,7 +205,7 @@
hx-target="#adherents-table-container"
hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${currentPage + 1}}|"
th:attr="hx-vals=|{&quot;page&quot;: ${currentPage + 1}}|"
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Suivant
</button>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Utilisateurs Connectés - 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; }
</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">Utilisateurs Connectés</h2>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6">
<div class="mb-6 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-900">
Sessions Actives (<span th:text="${activeCount}">0</span>)
</h3>
<button hx-get="/admin/sessions" hx-target="body" hx-swap="outerHTML" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
Rafraîchir
</button>
</div>
<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 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
<th class="py-3 px-6 font-medium text-left">Nom d'utilisateur</th>
<th class="py-3 px-6 font-medium text-left">Statut</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr th:if="${#lists.isEmpty(activeUsers)}">
<td colspan="2" class="py-8 text-center text-gray-500">Aucun utilisateur connecté pour le moment.</td>
</tr>
<tr th:each="username : ${activeUsers}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900 flex items-center">
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold mr-3">
<span th:text="${#strings.substring(username, 0, 1).toUpperCase()}">U</span>
</div>
<span th:text="${username}">username</span>
</td>
<td class="py-4 px-6">
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded-full">En ligne</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>
@@ -44,6 +44,7 @@
<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>
<a href="/admin/logs/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/admin/logs/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">Logs Paiements</a>
<a href="/admin/sessions" th:classappend="${requestURI != null and requestURI.startsWith('/admin/sessions') ? '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 Connectés</a>
</th:block>
</nav>
<div class="p-4 border-t border-gray-200">
@@ -20,9 +20,12 @@
<div class="flex-1 overflow-auto p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Liste des Catégories</h3>
<a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvelle Catégorie
</a>
<div class="flex items-center space-x-4">
<input type="text" id="searchInput" placeholder="Rechercher une catégorie..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
<a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvelle Catégorie
</a>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
@@ -64,5 +67,15 @@
</div>
</div>
</main>
<script>
document.getElementById('searchInput').addEventListener('keyup', function() {
let filter = this.value.toLowerCase();
let rows = document.querySelectorAll('tbody tr.hover\\:bg-gray-50');
rows.forEach(row => {
let text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
});
</script>
</body>
</html>
@@ -45,6 +45,15 @@
<label class="ml-2 block text-sm text-gray-700">Gérer les coupes Homme/Femme</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Type de public (Poste)</label>
<select th:field="*{typePublic}" 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="TOUS">Tous (Joueurs et Gardiens)</option>
<option value="JOUEUR">Joueurs de champ uniquement</option>
<option value="GARDIEN">Gardiens de but uniquement</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Tailles disponibles</label>
<input type="text" th:field="*{taillesDisponibles}" placeholder="ex: XS, S, M, L, XL ou 5/6 ANS, 7/8 ANS..." class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
@@ -20,9 +20,12 @@
<div class="flex-1 overflow-auto p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Liste des Équipements</h3>
<a th:href="@{/equipements/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvel Équipement
</a>
<div class="flex items-center space-x-4">
<input type="text" id="searchInput" placeholder="Rechercher un équipement..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
<a th:href="@{/equipements/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvel Équipement
</a>
</div>
</div>
<table class="w-full text-left border-collapse">
<thead>
@@ -51,5 +54,15 @@
</div>
</div>
</main>
<script>
document.getElementById('searchInput').addEventListener('keyup', function() {
let filter = this.value.toLowerCase();
let rows = document.querySelectorAll('tbody tr.hover\\:bg-gray-50');
rows.forEach(row => {
let text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
});
</script>
</body>
</html>
@@ -99,6 +99,20 @@
</div>
</div>
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700">Sexe <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="*{sexe}" value="MASCULIN" 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">Masculin</span>
</label>
<label class="flex items-center">
<input type="radio" th:field="*{sexe}" value="FEMININ" 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">Féminin</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
@@ -32,7 +32,7 @@
<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>
<h3 class="font-semibold text-yellow-900 mb-2 text-sm uppercase tracking-wide">Documents et informations à 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>
+1 -1
View File
@@ -13,7 +13,7 @@
<groupId>com.astalange</groupId>
<artifactId>as-talange-parent</artifactId>
<version>1.4-SNAPSHOT</version>
<version>1.4</version>
<packaging>pom</packaging>
<name>as-talange-parent</name>