28 Commits

Author SHA1 Message Date
ucef 19cee92437 feat: add active user session tracking and visualization in admin dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 4m1s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m34s
2026-07-03 17:18:14 +02:00
ucef 214ec08229 fix: resolve HTMX pagination bug on adherents list 2026-07-03 17:07:42 +02:00
ucef 62209ae659 fix(db): increase taille and numero length in dotation to 50
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m29s
2026-07-01 15:00:50 +02:00
ucef fc3373a1b4 feat(ui): ajout d'un tooltip sur les noms d'équipements tronqués dans la vue dotation
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m39s
2026-07-01 10:35:00 +02:00
ucef db3eb3ab9a feat: add sexe to pre-inscription form and entity 2026-07-01 09:32:42 +02:00
ucef d1f38f11f0 feat: add search filters to categories and equipements lists
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m16s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m23s
2026-06-30 15:11:16 +02:00
ucef acf9d6d301 fix: apply maillot price and default selection for all new registrations
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m0s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m19s
2026-06-30 14:42:59 +02:00
ucef 3066ff0e86 chore: bump version to 1.5-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m33s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m23s
2026-06-30 13:20:04 +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
ucef 8156c8f3d0 feat: gestion dynamique des équipements dans la configuration d'une catégorie
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m10s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-29 11:46:15 +02:00
ucef 7249b45533 feat: ajout du numéro de chèque et du commentaire sur les paiements 2026-06-29 11:28:52 +02:00
ucef 7e036c1966 feat: paramétrage dynamique des dotations (sexe, tailles, couleurs) 2026-06-29 11:11:53 +02:00
ucef 194986f880 feat: pré-sélection de la catégorie par âge et sexe lors de la création d'une licence
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 4m18s
2026-06-29 09:28:38 +02:00
ucef d834b8d0d3 fix: ajout du script de migration Flyway pour les nouveaux champs
- Script V30__add_ancienne_categorie_and_sexe.sql pour ajouter les champs en DB.

- Mise à jour des colonnes telephone, ancienne_categorie et sexe.
2026-06-29 09:14:18 +02:00
ucef e3abe53eb0 feat: amélioration des formulaires d'inscription et pré-inscription
- Rendre le champ téléphone obligatoire (entités et UI).

- Ajout du champ 'Ancienne catégorie' conditionné par la saisie de l'ancien club.

- Synchronisation de l'ancienne catégorie entre la pré-inscription et la fiche adhérent.
2026-06-29 09:03:45 +02:00
ucef 348d6e6ccf chore: bump version to 1.4-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m17s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-25 11:05:23 +02:00
ucef e670e38343 feat: harmoniser les documents demandés sur la page de succès de pré-inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m26s
2026-06-25 10:34:41 +02:00
ucef fe3a1ec36c fix: forcer le retour visuel de validation sur mobile (novalidate)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m12s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m12s
2026-06-25 10:07:51 +02:00
ucef 44ae524626 ci: ajout d'une vérification de démarrage après le déploiement
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m47s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m22s
2026-06-25 09:47:54 +02:00
ucef 829a69f1a5 fix: corriger l'indentation YAML qui empêchait l'application de démarrer 2026-06-25 09:45:12 +02:00
ucef f8336fb407 feat: affichage du numéro de version de l'application
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 22s
2026-06-25 09:19:59 +02:00
ucef ab1094d29a feat: préciser le justificatif de domicile pour les adhérents nés à l'étranger 2026-06-25 09:12:21 +02:00
39 changed files with 721 additions and 94 deletions
+17
View File
@@ -58,3 +58,20 @@ jobs:
echo "CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY:-1x00000000000000000000AA}" >> .env echo "CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY:-1x00000000000000000000AA}" >> .env
docker compose down app || true docker compose down app || true
docker compose up -d --build app docker compose up -d --build app
- name: Verify application startup
run: |
echo "Waiting for application to start..."
COUNT=0
while [ $COUNT -lt 30 ]; do
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
echo "Application is up and login page is accessible!"
exit 0
fi
echo "Waiting... ($COUNT/30)"
sleep 5
COUNT=$((COUNT+1))
done
echo "Application failed to start or login page is not accessible."
docker logs astalange_app
exit 1
+16 -1
View File
@@ -55,4 +55,19 @@ jobs:
docker compose down app || true docker compose down app || true
docker compose up -d --build app docker compose up -d --build app
- name: Verify application startup
run: |
echo "Waiting for application to start..."
COUNT=0
while [ $COUNT -lt 30 ]; do
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
echo "Application is up and login page is accessible!"
exit 0
fi
echo "Waiting... ($COUNT/30)"
sleep 5
COUNT=$((COUNT+1))
done
echo "Application failed to start or login page is not accessible."
docker logs astalange_app
exit 1
+3
View File
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -35,7 +35,7 @@ public class Adherent {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String email; private String email;
@Column(length = 20) @Column(nullable = false, length = 20)
private String telephone; private String telephone;
@Column(name = "representant_legal", length = 150) @Column(name = "representant_legal", length = 150)
@@ -47,6 +47,9 @@ public class Adherent {
@Column(name = "ancien_club", length = 150) @Column(name = "ancien_club", length = 150)
private String ancienClub; private String ancienClub;
@Column(name = "ancienne_categorie", length = 50)
private String ancienneCategorie;
private boolean residentTalange = true; private boolean residentTalange = true;
@Column(nullable = false, length = 10) @Column(nullable = false, length = 10)
@@ -99,6 +102,9 @@ public class Adherent {
public String getAncienClub() { return ancienClub; } public String getAncienClub() { return ancienClub; }
public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; } public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; }
public String getAncienneCategorie() { return ancienneCategorie; }
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
public boolean isResidentTalange() { return residentTalange; } public boolean isResidentTalange() { return residentTalange; }
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; } public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
@@ -18,6 +18,9 @@ public class Categorie {
@Column(nullable = false, length = 20) @Column(nullable = false, length = 20)
private String nom; private String nom;
@Column(nullable = false, length = 10)
private String sexe = "MASCULIN";
@Column(name = "annee_min") @Column(name = "annee_min")
private Integer anneeMin; private Integer anneeMin;
@@ -45,6 +48,9 @@ public class Categorie {
public String getNom() { return nom; } public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; } public void setNom(String nom) { this.nom = nom; }
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public Integer getAnneeMin() { return anneeMin; } public Integer getAnneeMin() { return anneeMin; }
public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; } public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; }
@@ -18,16 +18,19 @@ public class Dotation {
@JoinColumn(name = "equipement_id", nullable = false) @JoinColumn(name = "equipement_id", nullable = false)
private Equipement equipement; private Equipement equipement;
@Column(length = 10) @Column(length = 50)
private String taille; private String taille;
@Column(length = 50)
private String couleur;
@Column(length = 50) @Column(length = 50)
private String flocage; private String flocage;
@Column(nullable = false) @Column(nullable = false)
private Boolean fourni = false; private Boolean fourni = false;
@Column(length = 10) @Column(length = 50)
private String numero; private String numero;
@Column(nullable = false) @Column(nullable = false)
@@ -47,6 +50,9 @@ public class Dotation {
public String getTaille() { return taille; } public String getTaille() { return taille; }
public void setTaille(String taille) { this.taille = taille; } public void setTaille(String taille) { this.taille = taille; }
public String getCouleur() { return couleur; }
public void setCouleur(String couleur) { this.couleur = couleur; }
public String getFlocage() { return flocage; } public String getFlocage() { return flocage; }
public void setFlocage(String flocage) { this.flocage = flocage; } public void setFlocage(String flocage) { this.flocage = flocage; }
@@ -20,6 +20,18 @@ public class Equipement {
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String description; private String description;
@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;
@Column(name = "couleurs_disponibles", length = 500)
private String couleursDisponibles;
@ManyToOne(optional = false, fetch = FetchType.LAZY) @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false) @JoinColumn(name = "saison_id", nullable = false)
@@ -39,6 +51,18 @@ public class Equipement {
public String getDescription() { return description; } public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; } public void setDescription(String description) { this.description = description; }
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; }
public String getCouleursDisponibles() { return couleursDisponibles; }
public void setCouleursDisponibles(String couleursDisponibles) { this.couleursDisponibles = couleursDisponibles; }
public Saison getSaison() { return saison; } public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; } public void setSaison(Saison saison) { this.saison = saison; }
} }
@@ -29,6 +29,12 @@ public class Paiement {
@Column(name = "gestionnaire") @Column(name = "gestionnaire")
private String gestionnaire; private String gestionnaire;
@Column(name = "numero_cheque", length = 50)
private String numeroCheque;
@Column(name = "commentaire", columnDefinition = "TEXT")
private String commentaire;
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -48,4 +54,10 @@ public class Paiement {
public String getGestionnaire() { return gestionnaire; } public String getGestionnaire() { return gestionnaire; }
public void setGestionnaire(String gestionnaire) { this.gestionnaire = gestionnaire; } public void setGestionnaire(String gestionnaire) { this.gestionnaire = gestionnaire; }
public String getNumeroCheque() { return numeroCheque; }
public void setNumeroCheque(String numeroCheque) { this.numeroCheque = numeroCheque; }
public String getCommentaire() { return commentaire; }
public void setCommentaire(String commentaire) { this.commentaire = commentaire; }
} }
@@ -18,6 +18,9 @@ public class PreInscription {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String prenom; private String prenom;
@Column(nullable = false, length = 10)
private String sexe;
@Column(name = "date_naissance", nullable = false) @Column(name = "date_naissance", nullable = false)
private LocalDate dateNaissance; private LocalDate dateNaissance;
@@ -33,7 +36,7 @@ public class PreInscription {
@Column(nullable = false, length = 150) @Column(nullable = false, length = 150)
private String email; private String email;
@Column(length = 20) @Column(nullable = false, length = 20)
private String telephone; private String telephone;
@Column(name = "representant_legal", length = 150) @Column(name = "representant_legal", length = 150)
@@ -58,6 +61,9 @@ public class PreInscription {
@Column(name = "ancien_club", length = 150) @Column(name = "ancien_club", length = 150)
private String ancienClub; private String ancienClub;
@Column(name = "ancienne_categorie", length = 50)
private String ancienneCategorie = "NA";
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
public void setId(Long id) { this.id = id; } public void setId(Long id) { this.id = id; }
@@ -68,6 +74,9 @@ public class PreInscription {
public String getPrenom() { return prenom; } public String getPrenom() { return prenom; }
public void setPrenom(String prenom) { this.prenom = 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 LocalDate getDateNaissance() { return dateNaissance; }
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; } public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
@@ -106,4 +115,7 @@ public class PreInscription {
public String getAncienClub() { return ancienClub; } public String getAncienClub() { return ancienClub; }
public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; } public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; }
public String getAncienneCategorie() { return ancienneCategorie; }
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
} }
@@ -86,12 +86,21 @@ public class CategorieService {
List<Dotation> currentDotations = licence.getDotations(); List<Dotation> currentDotations = licence.getDotations();
List<Dotation> toRemove = new ArrayList<>(); 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) { for (Dotation d : currentDotations) {
boolean found = false; boolean found = false;
for (CategorieEquipement ce : categorieEquipements) { for (CategorieEquipement ce : categorieEquipements) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) { 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; break;
} }
} }
@@ -103,6 +112,17 @@ public class CategorieService {
// Add missing dotations // Add missing dotations
for (CategorieEquipement ce : categorieEquipements) { 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; boolean found = false;
for (Dotation d : currentDotations) { for (Dotation d : currentDotations) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) { if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
@@ -118,14 +138,13 @@ public class CategorieService {
newDotation.setLicence(licence); newDotation.setLicence(licence);
newDotation.setEquipement(ce.getEquipement()); newDotation.setEquipement(ce.getEquipement());
boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom()); boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom());
boolean isNouvelleAndU11 = false; boolean isNouvelle = false;
if (licence.getTypeDemande() != null && if (licence.getTypeDemande() != null &&
(licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) && (licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) &&
licence.getCategorie() != null && licence.getCategorie() != null) {
licence.getCategorie().isU11OrBelow()) { isNouvelle = true;
isNouvelleAndU11 = 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.setFourni(false);
newDotation.setTaille(""); newDotation.setTaille("");
newDotation.setNumero(""); newDotation.setNumero("");
@@ -46,8 +46,9 @@ public class PreInscriptionService {
adherent.setRepresentantLegal(pre.getRepresentantLegal()); adherent.setRepresentantLegal(pre.getRepresentantLegal());
adherent.setTypeDemande(pre.getTypeDemande()); adherent.setTypeDemande(pre.getTypeDemande());
adherent.setAncienClub(pre.getAncienClub()); adherent.setAncienClub(pre.getAncienClub());
adherent.setAncienneCategorie(pre.getAncienneCategorie());
adherent.setSexe(pre.getSexe());
// Par défaut // Par défaut
adherent.setSexe("MASCULIN");
adherent.setTypeMaillot("JOUEUR"); adherent.setTypeMaillot("JOUEUR");
adherent.setResidentTalange(false); adherent.setResidentTalange(false);
@@ -0,0 +1,10 @@
ALTER TABLE adherent ADD COLUMN ancienne_categorie VARCHAR(50);
ALTER TABLE pre_inscription ADD COLUMN ancienne_categorie VARCHAR(50) DEFAULT 'NA';
ALTER TABLE categorie ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
UPDATE adherent SET telephone = '0000000000' WHERE telephone IS NULL;
ALTER TABLE adherent ALTER COLUMN telephone SET NOT NULL;
UPDATE pre_inscription SET telephone = '0000000000' WHERE telephone IS NULL;
ALTER TABLE pre_inscription ALTER COLUMN telephone SET NOT NULL;
@@ -0,0 +1,5 @@
ALTER TABLE equipement ADD COLUMN gestion_sexe BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN tailles_disponibles VARCHAR(500);
ALTER TABLE equipement ADD COLUMN couleurs_disponibles VARCHAR(500);
ALTER TABLE dotation ADD COLUMN couleur VARCHAR(50);
@@ -0,0 +1,2 @@
ALTER TABLE paiement ADD COLUMN numero_cheque VARCHAR(50);
ALTER TABLE paiement ADD COLUMN commentaire TEXT;
@@ -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> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <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.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; 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 @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -41,6 +44,10 @@ public class SecurityConfig {
.logout(logout -> logout .logout(logout -> logout
.logoutSuccessUrl("/login?logout") .logoutSuccessUrl("/login?logout")
.permitAll() .permitAll()
)
.sessionManagement(session -> session
.maximumSessions(100)
.sessionRegistry(sessionRegistry())
); );
return http.build(); return http.build();
} }
@@ -57,4 +64,14 @@ public class SecurityConfig {
authProvider.setPasswordEncoder(passwordEncoder()); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; 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); Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null);
if (existing != null) { if (existing != null) {
boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance()); boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance());
boolean typeMaillotChanged = !existing.getTypeMaillot().equals(adherent.getTypeMaillot());
if (dateChanged) { if (dateChanged || typeMaillotChanged) {
int newBirthYear = adherent.getDateNaissance().getYear(); int newBirthYear = adherent.getDateNaissance().getYear();
List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId()); List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId());
for (Licence licence : licences) { for (Licence licence : licences) {
Saison saison = licence.getSaison(); if (dateChanged) {
Categorie newCat = categorieRepository.findBySaison(saison).stream() Saison saison = licence.getSaison();
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax()) Categorie newCat = categorieRepository.findBySaison(saison).stream()
.findFirst() .filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.orElse(null); .findFirst()
if (newCat != null) { .orElse(null);
licence.setCategorie(newCat); if (newCat != null) {
licence = licenceRepository.save(licence); licence.setCategorie(newCat);
categorieService.syncDotationsForLicence(licence); 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);
} }
} }
@@ -168,12 +168,14 @@ public class DotationController {
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id)); .orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
String taille = request.getParameter("taille_" + id); String taille = request.getParameter("taille_" + id);
String couleur = request.getParameter("couleur_" + id);
String numero = request.getParameter("numero_" + id); String numero = request.getParameter("numero_" + id);
String flocage = request.getParameter("flocage_" + id); String flocage = request.getParameter("flocage_" + id);
String choisiParam = request.getParameter("choisi_" + id); String choisiParam = request.getParameter("choisi_" + id);
String fourniParam = request.getParameter("fourni_" + id); String fourniParam = request.getParameter("fourni_" + id);
if (taille != null) dotation.setTaille(taille); if (taille != null) dotation.setTaille(taille);
if (couleur != null) dotation.setCouleur(couleur);
if (numero != null) dotation.setNumero(numero); if (numero != null) dotation.setNumero(numero);
if (flocage != null) dotation.setFlocage(flocage); if (flocage != null) dotation.setFlocage(flocage);
@@ -1,14 +1,23 @@
package com.astalange.web.controller; package com.astalange.web.controller;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@ControllerAdvice @ControllerAdvice
public class GlobalControllerAdvice { public class GlobalControllerAdvice {
@Value("${app.version}")
private String appVersion;
@ModelAttribute("requestURI") @ModelAttribute("requestURI")
public String requestURI(final HttpServletRequest request) { public String requestURI(final HttpServletRequest request) {
return request.getRequestURI(); return request.getRequestURI();
} }
@ModelAttribute("appVersion")
public String appVersion() {
return appVersion;
}
} }
@@ -40,6 +40,8 @@ public class PaiementController {
@RequestParam BigDecimal montant, @RequestParam BigDecimal montant,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
@RequestParam Long modePaiementId, @RequestParam Long modePaiementId,
@RequestParam(required = false) String numeroCheque,
@RequestParam(required = false) String commentaire,
Principal principal) { Principal principal) {
Licence licence = licenceRepository.findById(id) Licence licence = licenceRepository.findById(id)
@@ -58,6 +60,8 @@ public class PaiementController {
paiement.setModePaiement(mode); paiement.setModePaiement(mode);
paiement.setMontant(montant); paiement.setMontant(montant);
paiement.setDatePaiement(datePaiement); paiement.setDatePaiement(datePaiement);
paiement.setNumeroCheque(numeroCheque);
paiement.setCommentaire(commentaire);
if (principal != null) { if (principal != null) {
paiement.setGestionnaire(principal.getName()); paiement.setGestionnaire(principal.getName());
} }
@@ -82,6 +86,8 @@ public class PaiementController {
@RequestParam BigDecimal montant, @RequestParam BigDecimal montant,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
@RequestParam Long modePaiementId, @RequestParam Long modePaiementId,
@RequestParam(required = false) String numeroCheque,
@RequestParam(required = false) String commentaire,
@RequestParam(required = false) String redirect, @RequestParam(required = false) String redirect,
Principal principal) { Principal principal) {
@@ -106,6 +112,8 @@ public class PaiementController {
paiement.setModePaiement(mode); paiement.setModePaiement(mode);
paiement.setMontant(montant); paiement.setMontant(montant);
paiement.setDatePaiement(datePaiement); paiement.setDatePaiement(datePaiement);
paiement.setNumeroCheque(numeroCheque);
paiement.setCommentaire(commentaire);
if (principal != null) { if (principal != null) {
paiement.setGestionnaire(principal.getName()); paiement.setGestionnaire(principal.getName());
} }
@@ -133,9 +133,9 @@ public class PublicInscriptionController {
if (!categories.isEmpty()) { if (!categories.isEmpty()) {
Categorie cat = categories.get(0); Categorie cat = categories.get(0);
if (cat.getTarifBase() != null) { if (cat.getTarifBase() != null) {
BigDecimal prixMaillot = BigDecimal.ZERO; BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
if (cat.isU11OrBelow()) { if (prixMaillot == null) {
prixMaillot = cat.getEquipementPrixByNom("Maillot"); prixMaillot = BigDecimal.ZERO;
} }
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot); BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
tarifStr = tarifTotal.intValue() + ""; tarifStr = tarifTotal.intValue() + "";
@@ -147,10 +147,10 @@ public class PublicInscriptionController {
redirectAttributes.addFlashAttribute("tarif", tarifStr); redirectAttributes.addFlashAttribute("tarif", tarifStr);
redirectAttributes.addFlashAttribute("documents", java.util.Arrays.asList( redirectAttributes.addFlashAttribute("documents", java.util.Arrays.asList(
"Photo didentité", "Photo didentité",
"Carte didentité ou passeport de lenfant", "Carte didentité ou passeport de l'adhérent",
"Livret de famille (si pas de pièce didentité)", "Livret de famille (si pas de pièce didentité)",
"Adresse e-mail du représentant légal", "Adresse e-mail de l'adhérent (représentant légal en cas de mineur)",
"Pour les enfants nés à létranger : passeport + justificatif de domicile des parents" "Pour les adhérents nés à l'étranger : Passeport + justificatif de domicile (du représentant légal en cas de mineur)"
)); ));
redirectAttributes.addFlashAttribute("isNouvelle", true); redirectAttributes.addFlashAttribute("isNouvelle", true);
} }
@@ -175,9 +175,9 @@ public class PublicInscriptionController {
Categorie cat = categories.get(0); Categorie cat = categories.get(0);
if (cat.getTarifBase() != null) { if (cat.getTarifBase() != null) {
BigDecimal prixMaillot = BigDecimal.ZERO; BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
if (cat.isU11OrBelow()) { if (prixMaillot == null) {
prixMaillot = cat.getEquipementPrixByNom("Maillot"); prixMaillot = BigDecimal.ZERO;
} }
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot); BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
return tarifTotal.intValue() + ""; 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";
}
}
@@ -20,3 +20,6 @@ spring:
server: server:
port: 8080 port: 8080
app:
version: @project.version@
@@ -120,8 +120,8 @@
<!-- Téléphone --> <!-- Téléphone -->
<div> <div>
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label> <label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone <span class="text-red-500">*</span></label>
<input type="tel" id="telephone" th:field="*{telephone}" <input type="tel" id="telephone" th:field="*{telephone}" 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" 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' : ''"> 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> <p th:if="${#fields.hasErrors('telephone')}" th:errors="*{telephone}" class="mt-1 text-xs text-red-600"></p>
@@ -166,6 +166,29 @@
</div> </div>
</div> </div>
<div id="ancienneCategorieBlock" class="grid grid-cols-1 md:grid-cols-2 gap-6 hidden-block mt-6">
<div>
<label for="ancienneCategorie" class="block text-sm font-medium text-gray-700 mb-1">Ancienne catégorie <span class="text-red-500">*</span></label>
<select id="ancienneCategorie" th:field="*{ancienneCategorie}"
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="NA">NA (Je ne sais plus)</option>
<option value="U6">U6</option>
<option value="U7">U7</option>
<option value="U8">U8</option>
<option value="U9">U9</option>
<option value="U10">U10</option>
<option value="U11">U11</option>
<option value="U12">U12</option>
<option value="U13">U13</option>
<option value="U14">U14</option>
<option value="U15">U15</option>
<option value="U16">U16</option>
<option value="U17">U17</option>
<option value="Senior">Senior</option>
</select>
</div>
</div>
<!-- Removed Adresse --> <!-- Removed Adresse -->
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
@@ -255,6 +278,7 @@
<tr class="bg-gray-50 text-gray-500 uppercase font-medium"> <tr class="bg-gray-50 text-gray-500 uppercase font-medium">
<th class="py-2 px-3 text-left">Date</th> <th class="py-2 px-3 text-left">Date</th>
<th class="py-2 px-3 text-left">Mode</th> <th class="py-2 px-3 text-left">Mode</th>
<th class="py-2 px-3 text-left">Infos</th>
<th class="py-2 px-3 text-left">Géré par</th> <th class="py-2 px-3 text-left">Géré par</th>
<th class="py-2 px-3 text-right font-medium">Montant</th> <th class="py-2 px-3 text-right font-medium">Montant</th>
<th class="py-2 px-3 text-right font-medium pr-4">Actions</th> <th class="py-2 px-3 text-right font-medium pr-4">Actions</th>
@@ -266,6 +290,14 @@
<td class="py-2 px-3 text-gray-600"> <td class="py-2 px-3 text-gray-600">
<span class="px-2 py-0.5 rounded bg-gray-100 text-gray-700 font-mono text-[10px]" th:text="${paiement.modePaiement.nom}">Chèque</span> <span class="px-2 py-0.5 rounded bg-gray-100 text-gray-700 font-mono text-[10px]" th:text="${paiement.modePaiement.nom}">Chèque</span>
</td> </td>
<td class="py-2 px-3 text-gray-500 text-xs">
<div th:if="${paiement.numeroCheque != null and !paiement.numeroCheque.isEmpty()}" class="text-[10px]">
<span class="font-semibold text-gray-600">N°:</span> <span th:text="${paiement.numeroCheque}">123</span>
</div>
<div th:if="${paiement.commentaire != null and !paiement.commentaire.isEmpty()}" class="text-[10px] italic mt-0.5 text-gray-400 truncate max-w-[120px]" th:title="${paiement.commentaire}" th:text="${paiement.commentaire}">
Commentaire
</div>
</td>
<td class="py-2 px-3 text-gray-500 text-xs italic" th:text="${paiement.gestionnaire != null ? paiement.gestionnaire : '-'}">-</td> <td class="py-2 px-3 text-gray-500 text-xs italic" th:text="${paiement.gestionnaire != null ? paiement.gestionnaire : '-'}">-</td>
<td class="py-2 px-3 text-right font-semibold text-green-600" th:text="${paiement.montant + ' €'}">50.00 €</td> <td class="py-2 px-3 text-right font-semibold text-green-600" th:text="${paiement.montant + ' €'}">50.00 €</td>
<td class="py-2 px-3 text-right pr-4"> <td class="py-2 px-3 text-right pr-4">
@@ -277,7 +309,9 @@
th:data-mode-id="${paiement.modePaiement.id}" th:data-mode-id="${paiement.modePaiement.id}"
th:data-licence-id="${lic.id}" th:data-licence-id="${lic.id}"
th:data-max-amount="${lic.getResteAPayer().add(paiement.montant)}" th:data-max-amount="${lic.getResteAPayer().add(paiement.montant)}"
onclick="openEditPaiementModal(this.getAttribute('data-paiement-id'), this.getAttribute('data-montant'), this.getAttribute('data-date'), this.getAttribute('data-mode-id'), this.getAttribute('data-licence-id'), this.getAttribute('data-max-amount'))" th:data-cheque="${paiement.numeroCheque}"
th:data-commentaire="${paiement.commentaire}"
onclick="openEditPaiementModal(this.getAttribute('data-paiement-id'), this.getAttribute('data-montant'), this.getAttribute('data-date'), this.getAttribute('data-mode-id'), this.getAttribute('data-licence-id'), this.getAttribute('data-max-amount'), this.getAttribute('data-cheque'), this.getAttribute('data-commentaire'))"
class="text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 p-1.5 rounded transition-colors inline-flex items-center" class="text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 p-1.5 rounded transition-colors inline-flex items-center"
title="Modifier"> title="Modifier">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -324,24 +358,17 @@
<div> <div>
<div class="flex justify-between items-start mb-1 gap-1"> <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" <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: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> th:text="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
</div> </div>
<div class="text-[10px] text-gray-400 mb-2 truncate" th:title="${dot.equipement.description}" th:text="${dot.equipement.description != null and !dot.equipement.description.isEmpty() ? dot.equipement.description : 'Pas de description'}">Description</div> <div class="text-[10px] text-gray-400 mb-2 truncate" th:title="${dot.equipement.description}" th:text="${dot.equipement.description != null and !dot.equipement.description.isEmpty() ? dot.equipement.description : 'Pas de description'}">Description</div>
<!-- Special dynamic label for equipment style --> <!-- Special dynamic label for equipment style -->
<div class="mb-2" th:if="${dot.equipement.nom == 'Maillot' or dot.equipement.nom == 'Short' or dot.equipement.nom == 'Chaussettes'}"> <div class="mb-2" th:if="${dot.equipement.gestionSexe}">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-50 text-blue-700 border border-blue-100 dotation-style-label" <span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-50 text-blue-700 border border-blue-100 dotation-style-label"
th:data-equipement="${dot.equipement.nom}" th:data-equipement="${dot.equipement.nom}"
th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'} + ' - ' + ${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}"> th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'} + (${dot.equipement.nom == 'Maillot' or dot.equipement.nom == 'Short' or dot.equipement.nom == 'Chaussettes'} ? ' - ' + (${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}) : '')">
Modèle Homme - Joueur
</span>
</div>
<div class="mb-2" th:if="${dot.equipement.nom == 'Survêtement'}">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 border border-indigo-100 dotation-style-label"
th:data-equipement="${dot.equipement.nom}"
th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'}">
Modèle Homme Modèle Homme
</span> </span>
</div> </div>
@@ -358,17 +385,43 @@
</div> </div>
<div class="grid grid-cols-2 gap-1.5 mb-1.5"> <div class="grid grid-cols-2 gap-1.5 mb-1.5">
<div> <div th:if="${dot.equipement.taillesDisponibles != null and !dot.equipement.taillesDisponibles.trim().isEmpty()}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label> <label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" placeholder="S, M, L..." <select th:name="'taille_' + ${dot.id}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none"> <option value="">Choisir...</option>
<option th:each="t : ${#strings.arraySplit(dot.equipement.taillesDisponibles, ',')}"
th:value="${#strings.trim(t)}"
th:text="${#strings.trim(t)}"
th:selected="${dot.taille == #strings.trim(t)}"></option>
</select>
</div> </div>
<div th:if="${(dot.equipement.taillesDisponibles == null or dot.equipement.taillesDisponibles.trim().isEmpty()) and (dot.taille != null and !dot.taille.isEmpty())}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 outline-none bg-gray-50 text-gray-500" readonly>
</div>
<div th:if="${dot.equipement.couleursDisponibles != null and !dot.equipement.couleursDisponibles.trim().isEmpty()}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Couleur</label>
<select th:name="'couleur_' + ${dot.id}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
<option value="">Choisir...</option>
<option th:each="c : ${#strings.arraySplit(dot.equipement.couleursDisponibles, ',')}"
th:value="${#strings.trim(c)}"
th:text="${#strings.trim(c)}"
th:selected="${dot.couleur == #strings.trim(c)}"></option>
</select>
</div>
<div th:if="${(dot.equipement.couleursDisponibles == null or dot.equipement.couleursDisponibles.trim().isEmpty()) and (dot.couleur != null and !dot.couleur.isEmpty())}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Couleur</label>
<input type="text" th:name="'couleur_' + ${dot.id}" th:value="${dot.couleur}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 outline-none bg-gray-50 text-gray-500" readonly>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 mb-1.5">
<div> <div>
<label class="block text-[9px] uppercase font-bold text-gray-400">Numéro</label> <label class="block text-[9px] uppercase font-bold text-gray-400">Numéro</label>
<input type="text" th:name="'numero_' + ${dot.id}" th:value="${dot.numero}" placeholder="Ex: 10" <input type="text" th:name="'numero_' + ${dot.id}" th:value="${dot.numero}" placeholder="Ex: 10"
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none"> class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
</div> </div>
</div>
<div> <div>
<label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label> <label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label>
<input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur" <input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur"
@@ -408,6 +461,7 @@
th:data-nom="${cat.nom}" th:data-nom="${cat.nom}"
th:data-annee-min="${cat.anneeMin}" th:data-annee-min="${cat.anneeMin}"
th:data-annee-max="${cat.anneeMax}" th:data-annee-max="${cat.anneeMax}"
th:data-sexe="${cat.sexe}"
th:data-tarif-base="${cat.tarifBase}" th:data-tarif-base="${cat.tarifBase}"
th:data-tarif-exterieur="${cat.tarifExterieur}" th:data-tarif-exterieur="${cat.tarifExterieur}"
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option> th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
@@ -486,11 +540,19 @@
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label> <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"> <select id="addPaiementMode" 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" onchange="toggleChequeVisibility(this, 'addChequeBlock', 'addNumeroCheque')">
<option value="">Sélectionnez un mode...</option> <option value="">Sélectionnez un mode...</option>
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option> <option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:data-nom="${#strings.toLowerCase(mode.nom)}" th:text="${mode.nom}"></option>
</select> </select>
</div> </div>
<div id="addChequeBlock" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro du chèque <span class="text-red-500">*</span></label>
<input type="text" id="addNumeroCheque" name="numeroCheque" 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">Commentaire (facultatif)</label>
<textarea id="addCommentaire" name="commentaire" rows="2" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2"> <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="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> <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>
@@ -518,11 +580,19 @@
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label> <label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label>
<select id="editPaiementMode" 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"> <select id="editPaiementMode" 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" onchange="toggleChequeVisibility(this, 'editChequeBlock', 'editNumeroCheque')">
<option value="">Sélectionnez un mode...</option> <option value="">Sélectionnez un mode...</option>
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option> <option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:data-nom="${#strings.toLowerCase(mode.nom)}" th:text="${mode.nom}"></option>
</select> </select>
</div> </div>
<div id="editChequeBlock" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro du chèque <span class="text-red-500">*</span></label>
<input type="text" id="editNumeroCheque" name="numeroCheque" 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">Commentaire (facultatif)</label>
<textarea id="editCommentaire" name="commentaire" rows="2" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2"> <div class="items-center px-4 py-3 flex justify-end space-x-2">
<button type="button" onclick="closeEditPaiementModal()" 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="button" onclick="closeEditPaiementModal()" 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> <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>
@@ -581,25 +651,41 @@
document.getElementById('typeLicenceSelect').value = ''; document.getElementById('typeLicenceSelect').value = '';
// Pré-sélection de la catégorie selon l'année de naissance // Pré-sélection de la catégorie selon l'année de naissance et le sexe
const dateNaissanceStr = document.getElementById('dateNaissance').value; const dateNaissanceStr = document.getElementById('dateNaissance').value;
const adherentSexe = document.getElementById('sexe') ? document.getElementById('sexe').value : 'MASCULIN';
let foundCat = false; let foundCat = false;
let fallbackCatId = '';
let preSelectedCatId = ''; let preSelectedCatId = '';
if (dateNaissanceStr) { if (dateNaissanceStr) {
const birthYear = new Date(dateNaissanceStr).getFullYear(); const birthYear = new Date(dateNaissanceStr).getFullYear();
Array.from(catSelect.options).forEach(opt => { Array.from(catSelect.options).forEach(opt => {
if (!opt.value) return;
const min = parseInt(opt.getAttribute('data-annee-min')); const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max')); const max = parseInt(opt.getAttribute('data-annee-max'));
const catSexe = opt.getAttribute('data-sexe') || 'MASCULIN';
if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) { if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) {
catSelect.value = opt.value; if (catSexe === adherentSexe) {
preSelectedCatId = opt.value; catSelect.value = opt.value;
foundCat = true; preSelectedCatId = opt.value;
foundCat = true;
}
if (catSexe === 'MASCULIN') {
fallbackCatId = opt.value;
}
} }
}); });
} }
if (!foundCat && fallbackCatId) {
catSelect.value = fallbackCatId;
preSelectedCatId = fallbackCatId;
foundCat = true;
}
if (!foundCat) { if (!foundCat) {
catSelect.value = ''; catSelect.value = '';
} }
@@ -629,17 +715,35 @@
document.getElementById('paiementModal').classList.add('hidden'); document.getElementById('paiementModal').classList.add('hidden');
} }
function openEditPaiementModal(paiementId, montant, date, modePaiementId, licenceId, maxAmount) { function openEditPaiementModal(paiementId, montant, date, modePaiementId, licenceId, maxAmount, numeroCheque, commentaire) {
document.getElementById('editPaiementModal').classList.remove('hidden'); document.getElementById('editPaiementModal').classList.remove('hidden');
document.getElementById('editPaiementForm').action = '/paiements/' + paiementId + '/update'; document.getElementById('editPaiementForm').action = '/paiements/' + paiementId + '/update';
document.getElementById('editPaiementMontant').value = montant; document.getElementById('editPaiementMontant').value = montant;
document.getElementById('editPaiementMontant').max = maxAmount; document.getElementById('editPaiementMontant').max = maxAmount;
document.getElementById('editPaiementDate').value = date; document.getElementById('editPaiementDate').value = date;
document.getElementById('editPaiementMode').value = modePaiementId; document.getElementById('editPaiementMode').value = modePaiementId;
document.getElementById('editNumeroCheque').value = numeroCheque || '';
document.getElementById('editCommentaire').value = commentaire || '';
toggleChequeVisibility(document.getElementById('editPaiementMode'), 'editChequeBlock', 'editNumeroCheque');
} }
function closeEditPaiementModal() { function closeEditPaiementModal() {
document.getElementById('editPaiementModal').classList.add('hidden'); document.getElementById('editPaiementModal').classList.add('hidden');
} }
function toggleChequeVisibility(selectElement, blockId, inputId) {
const block = document.getElementById(blockId);
const input = document.getElementById(inputId);
const selectedOption = selectElement.options[selectElement.selectedIndex];
if (selectedOption && selectedOption.getAttribute('data-nom') && selectedOption.getAttribute('data-nom').includes('chèque')) {
block.classList.remove('hidden');
input.required = true;
} else {
block.classList.add('hidden');
input.required = false;
input.value = '';
}
}
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const dateInput = document.getElementById('dateNaissance'); const dateInput = document.getElementById('dateNaissance');
const repBlock = document.getElementById('representantBlock'); const repBlock = document.getElementById('representantBlock');
@@ -681,11 +785,33 @@
} }
} }
const ancienClubInput = document.getElementById('ancienClub');
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
if (ancienClubInput) {
ancienClubInput.addEventListener('input', function() {
if (this.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden-block');
ancienneCategorieSelect.required = true;
} else {
ancienneCategorieBlock.classList.add('hidden-block');
ancienneCategorieSelect.required = false;
}
});
if (ancienClubInput.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden-block');
ancienneCategorieSelect.required = true;
}
}
function updateLicencesCategoryAndPrices() { function updateLicencesCategoryAndPrices() {
if (!dateInput.value) return; if (!dateInput.value) return;
const dob = new Date(dateInput.value); const dob = new Date(dateInput.value);
const birthYear = dob.getFullYear(); const birthYear = dob.getFullYear();
const isResident = residentCheckbox ? residentCheckbox.checked : true; const isResident = residentCheckbox ? residentCheckbox.checked : true;
const adherentSexe = document.getElementById('sexe') ? document.getElementById('sexe').value : 'MASCULIN';
const categories = []; const categories = [];
const catSelect = document.getElementById('categorieSelect'); const catSelect = document.getElementById('categorieSelect');
@@ -695,17 +821,21 @@
const nom = opt.getAttribute('data-nom'); const nom = opt.getAttribute('data-nom');
const min = parseInt(opt.getAttribute('data-annee-min')); const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max')); const max = parseInt(opt.getAttribute('data-annee-max'));
const sexe = opt.getAttribute('data-sexe') || 'MASCULIN';
const tarifBase = parseFloat(opt.getAttribute('data-tarif-base') || '0'); const tarifBase = parseFloat(opt.getAttribute('data-tarif-base') || '0');
const tarifExterieur = parseFloat(opt.getAttribute('data-tarif-exterieur') || '0'); const tarifExterieur = parseFloat(opt.getAttribute('data-tarif-exterieur') || '0');
if (id) { if (id) {
categories.push({ id, nom, min, max, tarifBase, tarifExterieur }); categories.push({ id, nom, min, max, sexe, tarifBase, tarifExterieur });
} }
}); });
} }
if (categories.length === 0) return; if (categories.length === 0) return;
const matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max); let matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max && c.sexe === adherentSexe);
if (!matchingCat) {
matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max && c.sexe === 'MASCULIN');
}
if (!matchingCat) return; if (!matchingCat) return;
document.querySelectorAll('.licence-row').forEach(row => { document.querySelectorAll('.licence-row').forEach(row => {
@@ -853,7 +983,10 @@
} }
if (sexeSelect) { if (sexeSelect) {
sexeSelect.addEventListener('change', updateEquipmentStyleLabels); sexeSelect.addEventListener('change', function() {
updateEquipmentStyleLabels();
updateLicencesCategoryAndPrices();
});
} }
if (typeMaillotSelect) { if (typeMaillotSelect) {
typeMaillotSelect.addEventListener('change', updateEquipmentStyleLabels); typeMaillotSelect.addEventListener('change', updateEquipmentStyleLabels);
@@ -175,7 +175,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" 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"> class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Précédent Précédent
</button> </button>
@@ -190,7 +190,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" 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}" th:text="${pageNum + 1}"
class="px-3 py-1 rounded transition-colors" 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'}"> 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-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" 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"> class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Suivant Suivant
</button> </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> <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="/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/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> </th:block>
</nav> </nav>
<div class="p-4 border-t border-gray-200"> <div class="p-4 border-t border-gray-200">
@@ -51,6 +52,7 @@
<form th:action="@{/logout}" method="post"> <form th:action="@{/logout}" method="post">
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button> <button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
</form> </form>
<div class="mt-2 text-[10px] text-gray-400">v<span th:text="${appVersion}">1.0-SNAPSHOT</span></div>
</div> </div>
</aside> </aside>
</body> </body>
@@ -22,9 +22,18 @@
<form th:action="@{/categories}" th:object="${categorie}" method="post" class="space-y-6"> <form th:action="@{/categories}" th:object="${categorie}" method="post" class="space-y-6">
<input type="hidden" th:field="*{id}" /> <input type="hidden" th:field="*{id}" />
<div> <div class="grid grid-cols-2 gap-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label> <div>
<input type="text" 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 outline-none"> <label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label>
<input type="text" 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 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
<select th:field="*{sexe}" 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="MASCULIN">Masculine</option>
<option value="FEMININ">Féminine</option>
</select>
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -53,30 +62,95 @@
<div class="pt-4 border-t border-gray-100"> <div class="pt-4 border-t border-gray-100">
<label class="block text-sm font-medium text-gray-700 mb-2">Équipements liés à la catégorie</label> <label class="block text-sm font-medium text-gray-700 mb-2">Équipements liés à la catégorie</label>
<div class="space-y-3 max-h-64 overflow-y-auto">
<th:block th:each="equi : ${allEquipements}"> <div class="flex space-x-2 mb-4">
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100"> <select id="equipementToAdd" class="flex-1 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<div class="w-1/3"> <option value="">Sélectionner un équipement à ajouter...</option>
<th:block th:each="equi : ${allEquipements}">
<option th:value="${equi.id}" th:text="${equi.nom}"></option>
</th:block>
</select>
<button type="button" onclick="addEquipement()" class="px-4 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">Ajouter</button>
</div>
<div id="equipementsContainer" class="space-y-3 max-h-64 overflow-y-auto">
<th:block th:each="equi : ${allEquipements}" th:if="${categorie.hasEquipement(equi.id)}">
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row" th:id="'eq-row-' + ${equi.id}">
<div class="w-1/4">
<span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span> <span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span>
</div> </div>
<div class="w-1/3"> <div class="w-1/3">
<select th:name="'etatEquipement_' + ${equi.id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <select th:name="'etatEquipement_' + ${equi.id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="NON_LIE" th:selected="${!categorie.hasEquipement(equi.id)}">Non lié</option> <option value="OBLIGATOIRE" th:selected="${categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option>
<option value="OBLIGATOIRE" th:selected="${categorie.hasEquipement(equi.id) and categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option> <option value="OPTIONNEL" th:selected="${!categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
<option value="OPTIONNEL" th:selected="${categorie.hasEquipement(equi.id) and !categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
</select> </select>
</div> </div>
<div class="w-1/3 flex items-center space-x-2"> <div class="w-1/4 flex items-center space-x-2">
<label class="text-xs text-gray-500">Prix (€)</label> <label class="text-xs text-gray-500">Prix (€)</label>
<input type="number" step="0.01" th:name="'prixEquipement_' + ${equi.id}" <input type="number" step="0.01" th:name="'prixEquipement_' + ${equi.id}"
th:value="${categorie.getEquipementPrix(equi.id)}" th:value="${categorie.getEquipementPrix(equi.id)}"
class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div> </div>
<div class="w-auto">
<button type="button" th:onclick="'removeEquipement(' + ${equi.id} + ')'" class="text-red-600 hover:text-red-800 text-sm font-medium">Retirer</button>
</div>
</div> </div>
</th:block> </th:block>
</div> </div>
</div> </div>
<script>
function addEquipement() {
const select = document.getElementById('equipementToAdd');
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption.value) return;
const id = selectedOption.value;
const nom = selectedOption.text;
if (document.getElementById('eq-row-' + id)) {
alert("Cet équipement est déjà lié à la catégorie.");
return;
}
const container = document.getElementById('equipementsContainer');
const row = document.createElement('div');
row.className = 'flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row';
row.id = 'eq-row-' + id;
row.innerHTML = `
<div class="w-1/4">
<span class="block text-sm font-medium text-gray-900">${nom}</span>
</div>
<div class="w-1/3">
<select name="etatEquipement_${id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="OBLIGATOIRE">Obligatoire (inclus)</option>
<option value="OPTIONNEL">Optionnel (payant)</option>
</select>
</div>
<div class="w-1/4 flex items-center space-x-2">
<label class="text-xs text-gray-500">Prix (€)</label>
<input type="number" step="0.01" name="prixEquipement_${id}" value="0.00" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div class="w-auto">
<button type="button" onclick="removeEquipement(${id})" class="text-red-600 hover:text-red-800 text-sm font-medium">Retirer</button>
</div>
`;
container.appendChild(row);
select.value = '';
}
function removeEquipement(id) {
const row = document.getElementById('eq-row-' + id);
if (row) {
row.remove();
}
}
</script>
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
<a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a> <a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">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">Enregistrer</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>
@@ -20,9 +20,12 @@
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Liste des Catégories</h3> <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"> <div class="flex items-center space-x-4">
+ Nouvelle Catégorie <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> <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>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
@@ -43,7 +46,10 @@
<td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td> <td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td>
</tr> </tr>
<tr th:each="cat : ${categories}" class="hover:bg-gray-50 transition-colors"> <tr th:each="cat : ${categories}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900" th:text="${cat.nom}">U15</td> <td class="py-4 px-6">
<div class="font-medium text-gray-900" th:text="${cat.nom}">U15</div>
<div class="text-xs text-gray-500" th:text="${cat.sexe == 'FEMININ' ? 'Féminine' : 'Masculine'}">Masculine</div>
</td>
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMin}">2010</td> <td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMin}">2010</td>
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMax}">2011</td> <td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMax}">2011</td>
<td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td> <td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td>
@@ -61,5 +67,15 @@
</div> </div>
</div> </div>
</main> </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> </body>
</html> </html>
@@ -36,10 +36,37 @@
<label class="block text-sm font-medium text-gray-700 mb-1">Description (ex: Maillot domicile officiel)</label> <label class="block text-sm font-medium text-gray-700 mb-1">Description (ex: Maillot domicile officiel)</label>
<textarea th:field="*{description}" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea> <textarea th:field="*{description}" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
</div> </div>
<div class="pt-4 border-t border-gray-100">
<h3 class="text-sm font-medium text-gray-900 mb-4">Options de déclinaison (facultatif)</h3>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" th:field="*{gestionSexe}" class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<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">
<p class="text-xs text-gray-500 mt-1">Séparez les tailles par des virgules. Laissez vide si taille unique ou standard.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Couleurs disponibles</label>
<input type="text" th:field="*{couleursDisponibles}" placeholder="ex: Bleu, Rouge, Blanc..." class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<p class="text-xs text-gray-500 mt-1">Séparez les couleurs par des virgules. Laissez vide si couleur unique.</p>
</div>
</div>
</div>
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
<a th:href="@{/equipements}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a> <a th:href="@{/equipements}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">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">Enregistrer</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>
@@ -20,9 +20,12 @@
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Liste des Équipements</h3> <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"> <div class="flex items-center space-x-4">
+ Nouvel Équipement <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> <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> </div>
<table class="w-full text-left border-collapse"> <table class="w-full text-left border-collapse">
<thead> <thead>
@@ -51,5 +54,15 @@
</div> </div>
</div> </div>
</main> </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> </body>
</html> </html>
@@ -8,10 +8,14 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
body { font-family: 'Inter', sans-serif; } body { font-family: 'Inter', sans-serif; }
.was-validated input:invalid { .was-validated input:invalid, .was-validated select:invalid {
border-color: #ef4444 !important; border-color: #ef4444 !important;
background-color: #fef2f2 !important; background-color: #fef2f2 !important;
} }
.was-validated input[type="radio"]:invalid, .was-validated input[type="checkbox"]:invalid {
outline: 2px solid #ef4444;
outline-offset: 2px;
}
</style> </style>
</head> </head>
<body class="bg-gray-50 flex items-center justify-center min-h-screen py-12"> <body class="bg-gray-50 flex items-center justify-center min-h-screen py-12">
@@ -28,7 +32,7 @@
</ul> </ul>
</div> </div>
<form th:action="@{/inscription-public/formulaire}" th:object="${preInscription}" method="post" class="space-y-6"> <form th:action="@{/inscription-public/formulaire}" th:object="${preInscription}" method="post" class="space-y-6" novalidate>
<input type="hidden" name="token" th:value="${tokenUuid}" /> <input type="hidden" name="token" th:value="${tokenUuid}" />
<div class="space-y-3"> <div class="space-y-3">
@@ -48,7 +52,27 @@
<div id="nouvelleLicenceBlock" class="hidden space-y-4"> <div id="nouvelleLicenceBlock" class="hidden space-y-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Ancien club</label> <label class="block text-sm font-medium text-gray-700 mb-1">Ancien club</label>
<input type="text" th:field="*{ancienClub}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Nom du club (laisser vide si aucun)"> <input type="text" id="ancienClub" th:field="*{ancienClub}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Nom du club (laisser vide si aucun)">
</div>
<div id="ancienneCategorieBlock" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Ancienne catégorie <span class="text-red-500">*</span></label>
<select id="ancienneCategorie" th:field="*{ancienneCategorie}" 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="NA">NA (Je ne sais plus)</option>
<option value="U6">U6</option>
<option value="U7">U7</option>
<option value="U8">U8</option>
<option value="U9">U9</option>
<option value="U10">U10</option>
<option value="U11">U11</option>
<option value="U12">U12</option>
<option value="U13">U13</option>
<option value="U14">U14</option>
<option value="U15">U15</option>
<option value="U16">U16</option>
<option value="U17">U17</option>
<option value="Senior">Senior</option>
</select>
</div> </div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-lg"> <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-lg">
@@ -59,7 +83,7 @@
<li>Carte didentité ou passeport de l'adhérent</li> <li>Carte didentité ou passeport de l'adhérent</li>
<li>Livret de famille (si pas de pièce didentité)</li> <li>Livret de famille (si pas de pièce didentité)</li>
<li>Adresse e-mail de l'adhérent (représentant légal en cas de mineur)</li> <li>Adresse e-mail de l'adhérent (représentant légal en cas de mineur)</li>
<li>Pour les adhérents nés à létranger : passeport + justificatif de domicile des parents</li> <li>Pour les adhérents nés à l'étranger : Passeport + justificatif de domicile (du représentant légal en cas de mineur)</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -75,6 +99,20 @@
</div> </div>
</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 class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label> <label class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
@@ -109,8 +147,8 @@
<input type="email" th:field="*{email}" 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"> <input type="email" th:field="*{email}" 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>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label> <label class="block text-sm font-medium text-gray-700 mb-1">Téléphone <span class="text-red-500">*</span></label>
<input type="tel" 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 outline-none"> <input type="tel" th:field="*{telephone}" 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>
</div> </div>
@@ -131,6 +169,9 @@
<p class="text-[10px] text-gray-400 text-center mt-4 leading-tight"> <p class="text-[10px] text-gray-400 text-center mt-4 leading-tight">
Les données sont stockées de manière sécurisée, accessibles uniquement au bureau du club. Vous pouvez demander leur suppression à tout moment. Les données sont stockées de manière sécurisée, accessibles uniquement au bureau du club. Vous pouvez demander leur suppression à tout moment.
</p> </p>
<p class="text-[10px] text-gray-400 text-center mt-2">
v<span th:text="${appVersion}">1.0-SNAPSHOT</span>
</p>
</form> </form>
</div> </div>
@@ -139,6 +180,15 @@
form.addEventListener('submit', function(event) { form.addEventListener('submit', function(event) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault(); event.preventDefault();
let errorMsg = document.getElementById('form-error-msg');
if (!errorMsg) {
errorMsg = document.createElement('div');
errorMsg.id = 'form-error-msg';
errorMsg.className = 'mb-6 p-4 bg-red-50 border-l-4 border-red-500 text-red-700 text-sm rounded transition-all duration-300';
errorMsg.innerHTML = '<h3 class="font-bold">Erreur de validation</h3><p>Veuillez renseigner tous les champs obligatoires (encadrés en rouge).</p>';
form.insertBefore(errorMsg, form.firstChild);
}
} }
form.classList.add('was-validated'); form.classList.add('was-validated');
@@ -168,6 +218,27 @@
nouvelleLicenceBlock.classList.remove('hidden'); nouvelleLicenceBlock.classList.remove('hidden');
} }
const ancienClubInput = document.getElementById('ancienClub');
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
if (ancienClubInput) {
ancienClubInput.addEventListener('input', function() {
if (this.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden');
ancienneCategorieSelect.required = true;
} else {
ancienneCategorieBlock.classList.add('hidden');
ancienneCategorieSelect.required = false;
}
});
// Initial check
if (ancienClubInput.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden');
ancienneCategorieSelect.required = true;
}
}
document.getElementById('dateNaissance').addEventListener('change', function() { document.getElementById('dateNaissance').addEventListener('change', function() {
const dateInput = this.value; const dateInput = this.value;
const repBlock = document.getElementById('representantBlock'); const repBlock = document.getElementById('representantBlock');
@@ -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> <p class="text-sm font-semibold text-yellow-900">Tarif Licence : <span class="text-lg" th:text="${tarif}">210 €</span></p>
</div> </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"> <ul class="list-none space-y-2 mb-3">
<li th:each="doc : ${documents}" class="flex items-start"> <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> <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> <groupId>com.astalange</groupId>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>as-talange-parent</name> <name>as-talange-parent</name>