37 Commits

Author SHA1 Message Date
ucef 8ca20c9a4f chore: release v1.5
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m53s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m54s
2026-07-08 02:49:29 +02:00
ucef 92c9e80bc3 feat: update predefined shoe sizes list in public pre-registration
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m48s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m28s
2026-07-07 22:56:47 +02:00
ucef c94f89997f feat: add equipment duplication and name uniqueness validation 2026-07-07 22:56:29 +02:00
ucef af31766134 feat: use exhaustive standard sizes list in pre-registration and adjust layout
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m52s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m17s
2026-07-07 21:55:01 +02:00
ucef 3fc239683e feat: display and manage adherent clothing and shoe sizes 2026-07-07 21:54:52 +02:00
ucef a7c5b23198 feat: add category duplication functionality 2026-07-07 21:54:38 +02:00
ucef 29f9dc3bc2 feat: add flocage customization options to equipement configuration 2026-07-07 21:54:14 +02:00
ucef 5ee9be467b feat: add new fields in pre-registration and sync with adherent and dotations 2026-07-05 12:47:46 +02:00
ucef 6dd45deb8c feat: add commentaire field to licence modal 2026-07-05 12:38:39 +02:00
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
46 changed files with 1215 additions and 117 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</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</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,21 @@ 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;
@Column(name = "raison_changement_club")
private String raisonChangementClub;
@Column(name = "comment_connu_club")
private String commentConnuClub;
@Column(name = "taille_vetement", length = 50)
private String tailleVetement;
@Column(length = 50)
private String pointure;
private boolean residentTalange = true; private boolean residentTalange = true;
@Column(nullable = false, length = 10) @Column(nullable = false, length = 10)
@@ -99,6 +114,21 @@ 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 String getRaisonChangementClub() { return raisonChangementClub; }
public void setRaisonChangementClub(String raisonChangementClub) { this.raisonChangementClub = raisonChangementClub; }
public String getCommentConnuClub() { return commentConnuClub; }
public void setCommentConnuClub(String commentConnuClub) { this.commentConnuClub = commentConnuClub; }
public String getTailleVetement() { return tailleVetement; }
public void setTailleVetement(String tailleVetement) { this.tailleVetement = tailleVetement; }
public String getPointure() { return pointure; }
public void setPointure(String pointure) { this.pointure = pointure; }
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,27 @@ 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;
@Column(name = "has_flocage_prenom", nullable = false)
private boolean hasFlocagePrenom = false;
@Column(name = "has_flocage_initiales", nullable = false)
private boolean hasFlocageInitiales = false;
@Column(name = "has_flocage_numero", nullable = false)
private boolean hasFlocageNumero = false;
@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 +60,27 @@ 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 boolean isHasFlocagePrenom() { return hasFlocagePrenom; }
public void setHasFlocagePrenom(boolean hasFlocagePrenom) { this.hasFlocagePrenom = hasFlocagePrenom; }
public boolean isHasFlocageInitiales() { return hasFlocageInitiales; }
public void setHasFlocageInitiales(boolean hasFlocageInitiales) { this.hasFlocageInitiales = hasFlocageInitiales; }
public boolean isHasFlocageNumero() { return hasFlocageNumero; }
public void setHasFlocageNumero(boolean hasFlocageNumero) { this.hasFlocageNumero = hasFlocageNumero; }
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; }
} }
@@ -41,6 +41,9 @@ public class Licence {
@Column(name = "type_licence", length = 50) @Column(name = "type_licence", length = 50)
private String typeLicence; private String typeLicence;
@Column(columnDefinition = "TEXT")
private String commentaire;
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Paiement> paiements = new ArrayList<>(); private List<Paiement> paiements = new ArrayList<>();
@@ -128,6 +131,9 @@ public class Licence {
public String getTypeLicence() { return typeLicence; } public String getTypeLicence() { return typeLicence; }
public void setTypeLicence(String typeLicence) { this.typeLicence = typeLicence; } public void setTypeLicence(String typeLicence) { this.typeLicence = typeLicence; }
public String getCommentaire() { return commentaire; }
public void setCommentaire(String commentaire) { this.commentaire = commentaire; }
public List<Paiement> getPaiements() { return paiements; } public List<Paiement> getPaiements() { return paiements; }
public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; } public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; }
@@ -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,21 @@ 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";
@Column(name = "raison_changement_club")
private String raisonChangementClub;
@Column(name = "comment_connu_club")
private String commentConnuClub;
@Column(name = "taille_vetement", length = 50)
private String tailleVetement;
@Column(length = 50)
private String pointure;
// 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 +86,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 +127,19 @@ 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; }
public String getRaisonChangementClub() { return raisonChangementClub; }
public void setRaisonChangementClub(String raisonChangementClub) { this.raisonChangementClub = raisonChangementClub; }
public String getCommentConnuClub() { return commentConnuClub; }
public void setCommentConnuClub(String commentConnuClub) { this.commentConnuClub = commentConnuClub; }
public String getTailleVetement() { return tailleVetement; }
public void setTailleVetement(String tailleVetement) { this.tailleVetement = tailleVetement; }
public String getPointure() { return pointure; }
public void setPointure(String pointure) { this.pointure = pointure; }
} }
@@ -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,19 +138,29 @@ 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("");
if (licence.getAdherent() != null) { if (licence.getAdherent() != null) {
newDotation.setFlocage(licence.getAdherent().getNom().toUpperCase() + " " + licence.getAdherent().getPrenom()); String defaultFlocage = "";
if (ce.getEquipement().isHasFlocagePrenom() && ce.getEquipement().isHasFlocageInitiales()) {
String firstInitial = licence.getAdherent().getNom() != null && !licence.getAdherent().getNom().isEmpty() ? licence.getAdherent().getNom().substring(0, 1).toUpperCase() + "." : "";
defaultFlocage = (licence.getAdherent().getPrenom() != null ? licence.getAdherent().getPrenom() + " " : "") + firstInitial;
} else if (ce.getEquipement().isHasFlocagePrenom()) {
defaultFlocage = licence.getAdherent().getPrenom() != null ? licence.getAdherent().getPrenom() : "";
} else if (ce.getEquipement().isHasFlocageInitiales()) {
String firstInitP = licence.getAdherent().getPrenom() != null && !licence.getAdherent().getPrenom().isEmpty() ? licence.getAdherent().getPrenom().substring(0, 1).toUpperCase() : "";
String firstInitN = licence.getAdherent().getNom() != null && !licence.getAdherent().getNom().isEmpty() ? licence.getAdherent().getNom().substring(0, 1).toUpperCase() : "";
defaultFlocage = firstInitP + firstInitN;
}
newDotation.setFlocage(defaultFlocage.trim());
} }
currentDotations.add(newDotation); currentDotations.add(newDotation);
} }
@@ -138,4 +168,37 @@ public class CategorieService {
licenceRepository.save(licence); licenceRepository.save(licence);
} }
@Transactional
public Categorie duplicateCategorie(Long id) {
Categorie existing = categorieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
Categorie duplicated = new Categorie();
// Appending ' (dupliqué)' and truncating if necessary since nom is max 20 chars
String newNom = existing.getNom() + " (dupliqué)";
if (newNom.length() > 20) {
newNom = newNom.substring(0, 20);
}
duplicated.setNom(newNom);
duplicated.setSexe(existing.getSexe());
duplicated.setAnneeMin(existing.getAnneeMin());
duplicated.setAnneeMax(existing.getAnneeMax());
duplicated.setTarifBase(existing.getTarifBase());
duplicated.setTarifExterieur(existing.getTarifExterieur());
duplicated.setSaison(existing.getSaison());
List<CategorieEquipement> nouveauxEquipements = new ArrayList<>();
for (CategorieEquipement ce : existing.getCategorieEquipements()) {
CategorieEquipement newCe = new CategorieEquipement();
newCe.setCategorie(duplicated);
newCe.setEquipement(ce.getEquipement());
newCe.setObligatoire(ce.getObligatoire());
newCe.setPrix(ce.getPrix() != null ? ce.getPrix() : java.math.BigDecimal.ZERO);
nouveauxEquipements.add(newCe);
}
duplicated.getCategorieEquipements().addAll(nouveauxEquipements);
return categorieRepository.save(duplicated);
}
} }
@@ -46,8 +46,13 @@ 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());
adherent.setRaisonChangementClub(pre.getRaisonChangementClub());
adherent.setCommentConnuClub(pre.getCommentConnuClub());
adherent.setTailleVetement(pre.getTailleVetement());
adherent.setPointure(pre.getPointure());
// 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);
@@ -0,0 +1 @@
ALTER TABLE licence ADD COLUMN commentaire TEXT;
@@ -0,0 +1,9 @@
ALTER TABLE pre_inscription ADD COLUMN raison_changement_club VARCHAR(255);
ALTER TABLE pre_inscription ADD COLUMN comment_connu_club VARCHAR(255);
ALTER TABLE pre_inscription ADD COLUMN taille_vetement VARCHAR(50);
ALTER TABLE pre_inscription ADD COLUMN pointure VARCHAR(50);
ALTER TABLE adherent ADD COLUMN raison_changement_club VARCHAR(255);
ALTER TABLE adherent ADD COLUMN comment_connu_club VARCHAR(255);
ALTER TABLE adherent ADD COLUMN taille_vetement VARCHAR(50);
ALTER TABLE adherent ADD COLUMN pointure VARCHAR(50);
@@ -0,0 +1,31 @@
ALTER TABLE equipement ADD COLUMN has_flocage_prenom BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN has_flocage_initiales BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN has_flocage_numero BOOLEAN DEFAULT FALSE NOT NULL;
-- Activation automatique de l'option "numéro" pour les équipements ayant déjà des dotations avec un numéro
UPDATE equipement e
SET has_flocage_numero = TRUE
WHERE EXISTS (
SELECT 1 FROM dotation d WHERE d.equipement_id = e.id AND d.numero IS NOT NULL AND d.numero != ''
);
-- Activation automatique de l'option "prénom" (uniquement) pour les équipements ayant déjà des dotations avec un flocage
UPDATE equipement e
SET has_flocage_prenom = TRUE
WHERE EXISTS (
SELECT 1 FROM dotation d WHERE d.equipement_id = e.id AND d.flocage IS NOT NULL AND d.flocage != ''
);
UPDATE dotation
SET flocage = a.prenom
FROM licence l
JOIN adherent a ON l.adherent_id = a.id
WHERE dotation.licence_id = l.id
AND dotation.flocage IS NOT NULL
AND dotation.flocage != ''
AND EXISTS (
SELECT 1 FROM equipement e
WHERE e.id = dotation.equipement_id
AND e.has_flocage_prenom = TRUE
AND e.has_flocage_initiales = FALSE
);
+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</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();
}
} }
@@ -48,6 +48,8 @@ public class AdherentController {
@org.springframework.web.bind.annotation.RequestParam(required = false) String licence, @org.springframework.web.bind.annotation.RequestParam(required = false) String licence,
@org.springframework.web.bind.annotation.RequestParam(required = false) String email, @org.springframework.web.bind.annotation.RequestParam(required = false) String email,
@org.springframework.web.bind.annotation.RequestParam(required = false) String paiement, @org.springframework.web.bind.annotation.RequestParam(required = false) String paiement,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "nom") String sortField,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "asc") String sortDirection,
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "0") int page, @org.springframework.web.bind.annotation.RequestParam(defaultValue = "0") int page,
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "20") int size, @org.springframework.web.bind.annotation.RequestParam(defaultValue = "20") int size,
Model model) { Model model) {
@@ -113,7 +115,29 @@ public class AdherentController {
model.addAttribute("paiementFilter", paiement.trim()); model.addAttribute("paiementFilter", paiement.trim());
} }
// 3. Paginate // 3. Sort
java.util.Comparator<Adherent> comparator = (a1, a2) -> 0;
if ("nom".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getNom() != null ? a.getNom() : "");
} else if ("prenom".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getPrenom() != null ? a.getPrenom() : "");
} else if ("categorie".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> (a.getLicenceActuelle() != null && a.getLicenceActuelle().getCategorie() != null && a.getLicenceActuelle().getCategorie().getNom() != null) ? a.getLicenceActuelle().getCategorie().getNom() : "");
} else if ("licence".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> (a.getLicenceActuelle() != null && a.getLicenceActuelle().getNumeroLicence() != null) ? a.getLicenceActuelle().getNumeroLicence() : "");
} else if ("email".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getEmail() != null ? a.getEmail() : "");
}
if ("desc".equalsIgnoreCase(sortDirection)) {
comparator = comparator.reversed();
}
adherents.sort(comparator);
model.addAttribute("sortField", sortField);
model.addAttribute("sortDirection", sortDirection);
// 4. Paginate
int totalElements = adherents.size(); int totalElements = adherents.size();
int totalPages = (int) Math.ceil((double) totalElements / size); int totalPages = (int) Math.ceil((double) totalElements / size);
if (page < 0) page = 0; if (page < 0) page = 0;
@@ -177,21 +201,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);
} }
} }
@@ -36,7 +36,9 @@ public class CategorieController {
@GetMapping("/new") @GetMapping("/new")
public String showCreateForm(Model model) { public String showCreateForm(Model model) {
model.addAttribute("categorie", new Categorie()); if (!model.containsAttribute("categorie")) {
model.addAttribute("categorie", new Categorie());
}
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of()); model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of());
return "parametrage/categories_form"; return "parametrage/categories_form";
@@ -59,7 +61,26 @@ public class CategorieController {
@PostMapping @PostMapping
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie, public String saveCategorie(@ModelAttribute("categorie") Categorie categorie,
jakarta.servlet.http.HttpServletRequest request) { jakarta.servlet.http.HttpServletRequest request,
org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
Saison activeSaison;
if (categorie.getId() == null) {
activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
} else {
Categorie existing = categorieRepository.findById(categorie.getId()).orElse(null);
activeSaison = existing != null ? existing.getSaison() : null;
}
if (activeSaison != null) {
for (Categorie c : categorieRepository.findBySaison(activeSaison)) {
if (c.getNom().equalsIgnoreCase(categorie.getNom()) && (categorie.getId() == null || !c.getId().equals(categorie.getId()))) {
redirectAttributes.addFlashAttribute("errorMessage", "Erreur : La catégorie '" + categorie.getNom() + "' existe déjà pour cette saison.");
redirectAttributes.addFlashAttribute("categorie", categorie);
return categorie.getId() == null ? "redirect:/categories/new" : "redirect:/categories/" + categorie.getId() + "/edit";
}
}
}
java.util.Map<Long, com.astalange.core.service.CategorieService.EquipementConfig> configs = new java.util.HashMap<>(); java.util.Map<Long, com.astalange.core.service.CategorieService.EquipementConfig> configs = new java.util.HashMap<>();
@@ -93,4 +114,10 @@ public class CategorieController {
categorieRepository.delete(categorie); categorieRepository.delete(categorie);
return "redirect:/categories"; return "redirect:/categories";
} }
@PostMapping("/{id}/duplicate")
public String duplicateCategorie(@PathVariable Long id) {
categorieService.duplicateCategorie(id);
return "redirect:/categories";
}
} }
@@ -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);
@@ -33,34 +33,78 @@ public class EquipementController {
@GetMapping("/new") @GetMapping("/new")
public String showCreateForm(Model model) { public String showCreateForm(Model model) {
model.addAttribute("equipement", new Equipement()); if (!model.containsAttribute("equipement")) {
model.addAttribute("equipement", new Equipement());
}
return "parametrage/equipements_form"; return "parametrage/equipements_form";
} }
@GetMapping("/{id}/edit") @GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model) { public String showEditForm(@PathVariable Long id, Model model) {
Equipement equipement = equipementRepository.findById(id) if (!model.containsAttribute("equipement")) {
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + id)); Equipement equipement = equipementRepository.findById(id)
model.addAttribute("equipement", equipement); .orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + id));
model.addAttribute("equipement", equipement);
}
return "parametrage/equipements_form"; return "parametrage/equipements_form";
} }
@PostMapping @PostMapping
public String saveEquipement(@ModelAttribute("equipement") Equipement equipement) { public String saveEquipement(@ModelAttribute("equipement") Equipement equipement,
org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
Saison activeSaison;
if (equipement.getId() == null) { if (equipement.getId() == null) {
Saison activeSaison = saisonRepository.findByEstActiveTrue() activeSaison = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée")); .orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
equipement.setSaison(activeSaison); equipement.setSaison(activeSaison);
} else { } else {
Equipement existing = equipementRepository.findById(equipement.getId()) Equipement existing = equipementRepository.findById(equipement.getId())
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + equipement.getId())); .orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + equipement.getId()));
equipement.setSaison(existing.getSaison()); equipement.setSaison(existing.getSaison());
activeSaison = existing.getSaison();
} }
if (activeSaison != null) {
for (Equipement e : equipementRepository.findBySaison(activeSaison)) {
if (e.getNom().equalsIgnoreCase(equipement.getNom()) && (equipement.getId() == null || !e.getId().equals(equipement.getId()))) {
redirectAttributes.addFlashAttribute("errorMessage", "Erreur : L'équipement '" + equipement.getNom() + "' existe déjà pour cette saison.");
redirectAttributes.addFlashAttribute("equipement", equipement);
return equipement.getId() == null ? "redirect:/equipements/new" : "redirect:/equipements/" + equipement.getId() + "/edit";
}
}
}
equipementRepository.save(equipement); equipementRepository.save(equipement);
return "redirect:/equipements"; return "redirect:/equipements";
} }
@PostMapping("/{id}/duplicate")
public String duplicateEquipement(@PathVariable Long id) {
Equipement existing = equipementRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + id));
Equipement duplicated = new Equipement();
String newNom = existing.getNom() + " (dupliqué)";
if (newNom.length() > 100) {
newNom = newNom.substring(0, 100);
}
duplicated.setNom(newNom);
duplicated.setReference(existing.getReference());
duplicated.setDescription(existing.getDescription());
duplicated.setGestionSexe(existing.isGestionSexe());
duplicated.setTypePublic(existing.getTypePublic());
duplicated.setTaillesDisponibles(existing.getTaillesDisponibles());
duplicated.setCouleursDisponibles(existing.getCouleursDisponibles());
duplicated.setHasFlocagePrenom(existing.isHasFlocagePrenom());
duplicated.setHasFlocageInitiales(existing.isHasFlocageInitiales());
duplicated.setHasFlocageNumero(existing.isHasFlocageNumero());
duplicated.setSaison(existing.getSaison());
equipementRepository.save(duplicated);
return "redirect:/equipements";
}
@PostMapping("/{id}/delete") @PostMapping("/{id}/delete")
public String deleteEquipement(@PathVariable Long id) { public String deleteEquipement(@PathVariable Long id) {
Equipement equipement = equipementRepository.findById(id) Equipement equipement = equipementRepository.findById(id)
@@ -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;
}
} }
@@ -49,6 +49,7 @@ public class LicenceController {
@RequestParam(required = false) String numeroLicence, @RequestParam(required = false) String numeroLicence,
@RequestParam(required = false) String typeDemande, @RequestParam(required = false) String typeDemande,
@RequestParam(required = false) String typeLicence, @RequestParam(required = false) String typeLicence,
@RequestParam(required = false) String commentaire,
@RequestParam(required = false) Long equipeId) { @RequestParam(required = false) Long equipeId) {
Adherent adherent = adherentRepository.findById(id) Adherent adherent = adherentRepository.findById(id)
@@ -73,6 +74,7 @@ public class LicenceController {
licence.setNumeroLicence(numeroLicence); licence.setNumeroLicence(numeroLicence);
licence.setTypeDemande(typeDemande); licence.setTypeDemande(typeDemande);
licence.setTypeLicence(typeLicence); licence.setTypeLicence(typeLicence);
licence.setCommentaire(commentaire);
if (equipeId != null) { if (equipeId != null) {
Equipe equipe = equipeRepository.findById(equipeId) Equipe equipe = equipeRepository.findById(equipeId)
@@ -97,6 +99,7 @@ public class LicenceController {
@RequestParam(required = false) String numeroLicence, @RequestParam(required = false) String numeroLicence,
@RequestParam(required = false) String typeDemande, @RequestParam(required = false) String typeDemande,
@RequestParam(required = false) String typeLicence, @RequestParam(required = false) String typeLicence,
@RequestParam(required = false) String commentaire,
@RequestParam(required = false) Long equipeId) { @RequestParam(required = false) Long equipeId) {
Licence licence = licenceRepository.findById(licenceId) Licence licence = licenceRepository.findById(licenceId)
@@ -110,6 +113,7 @@ public class LicenceController {
licence.setNumeroLicence(numeroLicence); licence.setNumeroLicence(numeroLicence);
licence.setTypeDemande(typeDemande); licence.setTypeDemande(typeDemande);
licence.setTypeLicence(typeLicence); licence.setTypeLicence(typeLicence);
licence.setCommentaire(commentaire);
if (equipeId != null) { if (equipeId != null) {
Equipe equipe = equipeRepository.findById(equipeId) Equipe equipe = equipeRepository.findById(equipeId)
@@ -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());
} }
@@ -7,7 +7,9 @@ import com.astalange.core.repository.PreInscriptionRepository;
import com.astalange.core.repository.TokenPreInscriptionRepository; import com.astalange.core.repository.TokenPreInscriptionRepository;
import com.astalange.core.repository.SaisonRepository; import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.repository.CategorieRepository; import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.entity.Categorie; import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Equipement;
import com.astalange.core.service.CaptchaValidationService; import com.astalange.core.service.CaptchaValidationService;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@@ -20,6 +22,8 @@ import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import java.util.List; import java.util.List;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Set;
import java.util.TreeSet;
@Controller @Controller
@RequestMapping("/inscription-public") @RequestMapping("/inscription-public")
@@ -30,17 +34,20 @@ public class PublicInscriptionController {
private final PreInscriptionRepository preInscriptionRepository; private final PreInscriptionRepository preInscriptionRepository;
private final SaisonRepository saisonRepository; private final SaisonRepository saisonRepository;
private final CategorieRepository categorieRepository; private final CategorieRepository categorieRepository;
private final EquipementRepository equipementRepository;
public PublicInscriptionController(CaptchaValidationService captchaValidationService, public PublicInscriptionController(CaptchaValidationService captchaValidationService,
TokenPreInscriptionRepository tokenRepository, TokenPreInscriptionRepository tokenRepository,
PreInscriptionRepository preInscriptionRepository, PreInscriptionRepository preInscriptionRepository,
SaisonRepository saisonRepository, SaisonRepository saisonRepository,
CategorieRepository categorieRepository) { CategorieRepository categorieRepository,
EquipementRepository equipementRepository) {
this.captchaValidationService = captchaValidationService; this.captchaValidationService = captchaValidationService;
this.tokenRepository = tokenRepository; this.tokenRepository = tokenRepository;
this.preInscriptionRepository = preInscriptionRepository; this.preInscriptionRepository = preInscriptionRepository;
this.saisonRepository = saisonRepository; this.saisonRepository = saisonRepository;
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.equipementRepository = equipementRepository;
} }
@Value("${captcha.sitekey:1x00000000000000000000AA}") @Value("${captcha.sitekey:1x00000000000000000000AA}")
@@ -97,6 +104,18 @@ public class PublicInscriptionController {
model.addAttribute("preInscription", new PreInscription()); model.addAttribute("preInscription", new PreInscription());
model.addAttribute("tokenUuid", tokenUuid); model.addAttribute("tokenUuid", tokenUuid);
// Fetch tailles and pointures (Standard exhaustive list)
Set<String> tailles = new java.util.LinkedHashSet<>(java.util.Arrays.asList(
"5/6ANS (Taille 116cm)", "7/8ANS (Taille 128cm)", "9/10ANS (Taille 140cm)",
"11/12ANS (Taille 152cm)", "13/14ANS (Taille 164cm)", "XS", "S", "M", "L", "XL", "XXL"
));
Set<String> pointures = new java.util.LinkedHashSet<>(java.util.Arrays.asList(
"27/30 (Taille 0)", "31/34 (Taille 1)", "35/38 (Taille 2)", "39/42 (Taille 3)", "43/46 (Taille 4)"
));
model.addAttribute("taillesDisponibles", tailles);
model.addAttribute("pointuresDisponibles", pointures);
return "public/formulaire"; return "public/formulaire";
} }
@@ -133,9 +152,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 +166,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 +194,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,53 @@
</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>
<label for="raisonChangementClub" class="block text-sm font-medium text-gray-700 mb-1">Raison du changement</label>
<input type="text" id="raisonChangementClub" th:field="*{raisonChangementClub}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div>
<label for="tailleVetement" class="block text-sm font-medium text-gray-700 mb-1">Taille équipement</label>
<input type="text" id="tailleVetement" th:field="*{tailleVetement}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
</div>
<div>
<label for="pointure" class="block text-sm font-medium text-gray-700 mb-1">Pointure chaussettes</label>
<input type="text" id="pointure" th:field="*{pointure}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
</div>
</div>
<div class="mt-6" th:classappend="${adherent.typeDemande == 'RENOUVELLEMENT'} ? 'hidden-block' : ''">
<label for="commentConnuClub" class="block text-sm font-medium text-gray-700 mb-1">Comment a-t-il connu le club ?</label>
<input type="text" id="commentConnuClub" th:field="*{commentConnuClub}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
</div>
<!-- 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">
@@ -236,7 +283,8 @@
th:data-type-demande="${lic.typeDemande}" th:data-type-demande="${lic.typeDemande}"
th:data-type-licence="${lic.typeLicence}" th:data-type-licence="${lic.typeLicence}"
th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}" th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-type-demande'), this.getAttribute('data-type-licence'), this.getAttribute('data-equipe-id'))" th:data-commentaire="${lic.commentaire}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-type-demande'), this.getAttribute('data-type-licence'), this.getAttribute('data-equipe-id'), this.getAttribute('data-commentaire'))"
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button> class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
<button th:if="${lic.getResteAPayer() > 0}" <button th:if="${lic.getResteAPayer() > 0}"
type="button" type="button"
@@ -255,6 +303,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 +315,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 +334,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 +383,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,22 +410,57 @@
</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="" th:selected="${dot.taille == null or dot.taille.isEmpty()}">Choisir...</option>
<option th:if="${dot.taille != null and !dot.taille.isEmpty()}"
th:value="${dot.taille}"
th:text="${dot.taille} + ' (Actuelle)'"
selected></option>
<option th:each="t : ${#strings.arraySplit(dot.equipement.taillesDisponibles, ',')}"
th:if="${dot.taille == null or dot.taille != #strings.trim(t)}"
th:value="${#strings.trim(t)}"
th:text="${#strings.trim(t)}"
th:selected="${dot.taille == null and (#strings.trim(t) == adherent.tailleVetement or #strings.trim(t) == adherent.pointure)}"></option>
</select>
</div> </div>
<div> <div th:if="${(dot.equipement.taillesDisponibles == null or dot.equipement.taillesDisponibles.trim().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 != null and !dot.taille.isEmpty() ? dot.taille : (adherent.tailleVetement != null ? adherent.tailleVetement : '')}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none" placeholder="Taille/Pointure">
</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="" th:selected="${dot.couleur == null or dot.couleur.isEmpty()}">Choisir...</option>
<option th:if="${dot.couleur != null and !dot.couleur.isEmpty()}"
th:value="${dot.couleur}"
th:text="${dot.couleur} + ' (Actuelle)'"
selected></option>
<option th:each="c : ${#strings.arraySplit(dot.equipement.couleursDisponibles, ',')}"
th:if="${dot.couleur == null or dot.couleur != #strings.trim(c)}"
th:value="${#strings.trim(c)}"
th:text="${#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 th:if="${dot.equipement.hasFlocageNumero}">
<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 th:if="${dot.equipement.hasFlocagePrenom or dot.equipement.hasFlocageInitiales}">
<div> <label class="block text-[9px] uppercase font-bold text-gray-400 whitespace-nowrap truncate" title="Flocage" th:text="'Flocage (' + (${dot.equipement.hasFlocagePrenom and dot.equipement.hasFlocageInitiales} ? 'Prénom/Init.' : (${dot.equipement.hasFlocagePrenom} ? 'Prénom' : 'Initiales')) + ')'">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="À floquer"
<input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur" 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 class="flex items-center pt-1.5"> <div class="flex items-center pt-1.5">
<label class="flex items-center space-x-1 cursor-pointer select-none"> <label class="flex items-center space-x-1 cursor-pointer select-none">
<input type="checkbox" th:name="'fourni_' + ${dot.id}" th:checked="${dot.fourni}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"> <input type="checkbox" th:name="'fourni_' + ${dot.id}" th:checked="${dot.fourni}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
@@ -408,6 +495,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>
@@ -459,6 +547,10 @@
<option value="Payée">Payée</option> <option value="Payée">Payée</option>
</select> </select>
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Commentaire</label>
<textarea id="licenceCommentaireInput" name="commentaire" 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" placeholder="Commentaire optionnel..."></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="closeLicenceModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button> <button type="button" onclick="closeLicenceModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button> <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>
@@ -486,11 +578,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 +618,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>
@@ -533,7 +641,7 @@
</div> </div>
<script th:inline="javascript"> <script th:inline="javascript">
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', typeDemande = '', typeLicence = '', equipeId = '') { function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', typeDemande = '', typeLicence = '', equipeId = '', commentaire = '') {
document.getElementById('licenceModal').classList.remove('hidden'); document.getElementById('licenceModal').classList.remove('hidden');
const form = document.getElementById('licenceForm'); const form = document.getElementById('licenceForm');
const title = document.getElementById('licenceModalTitle'); const title = document.getElementById('licenceModalTitle');
@@ -552,6 +660,7 @@
document.getElementById('etatSelect').value = etat; document.getElementById('etatSelect').value = etat;
document.getElementById('typeDemandeSelect').value = typeDemande; document.getElementById('typeDemandeSelect').value = typeDemande;
document.getElementById('typeLicenceSelect').value = typeLicence; document.getElementById('typeLicenceSelect').value = typeLicence;
document.getElementById('licenceCommentaireInput').value = commentaire || '';
// Fetch and populate teams // Fetch and populate teams
if (categorieId) { if (categorieId) {
@@ -567,6 +676,7 @@
form.action = '/adherents/' + adherentId + '/licences'; form.action = '/adherents/' + adherentId + '/licences';
document.getElementById('numeroLicenceInput').value = ''; document.getElementById('numeroLicenceInput').value = '';
document.getElementById('etatSelect').value = 'Brouillon'; document.getElementById('etatSelect').value = 'Brouillon';
document.getElementById('licenceCommentaireInput').value = '';
// Pré-remplissage du type de demande // Pré-remplissage du type de demande
const adherentTypeDemande = document.getElementById('typeDemande') ? document.getElementById('typeDemande').value : ''; const adherentTypeDemande = document.getElementById('typeDemande') ? document.getElementById('typeDemande').value : '';
@@ -581,25 +691,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 +755,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 +825,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 +861,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 +1023,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);
@@ -58,16 +58,63 @@
<table class="w-full text-left border-collapse min-w-[1000px]"> <table class="w-full text-left border-collapse min-w-[1000px]">
<thead> <thead>
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200"> <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</th> <th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('nom')">
<th class="py-3 px-6 font-medium text-left">Prénom</th> <div class="flex items-center space-x-1">
<th class="py-3 px-6 font-medium text-left">Catégorie</th> <span>Nom</span>
<th class="py-3 px-6 font-medium text-center">N° Licence</th> <span th:if="${sortField != 'nom'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
<th class="py-3 px-6 font-medium text-left">Email</th> <span th:if="${sortField == 'nom'}">
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
</span>
</div>
</th>
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('prenom')">
<div class="flex items-center space-x-1">
<span>Prénom</span>
<span th:if="${sortField != 'prenom'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
<span th:if="${sortField == 'prenom'}">
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
</span>
</div>
</th>
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('categorie')">
<div class="flex items-center space-x-1">
<span>Catégorie</span>
<span th:if="${sortField != 'categorie'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
<span th:if="${sortField == 'categorie'}">
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
</span>
</div>
</th>
<th class="py-3 px-6 font-medium text-center cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('licence')">
<div class="flex items-center justify-center space-x-1">
<span>N° Licence</span>
<span th:if="${sortField != 'licence'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
<span th:if="${sortField == 'licence'}">
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
</span>
</div>
</th>
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('email')">
<div class="flex items-center space-x-1">
<span>Email</span>
<span th:if="${sortField != 'email'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
<span th:if="${sortField == 'email'}">
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
</span>
</div>
</th>
<th class="py-3 px-6 font-medium text-center">Paiement</th> <th class="py-3 px-6 font-medium text-center">Paiement</th>
<th class="py-3 px-6 font-medium text-right">Actions</th> <th class="py-3 px-6 font-medium text-right">Actions</th>
</tr> </tr>
<!-- Filter Row --> <!-- Filter Row -->
<tr id="filter-row" class="bg-gray-100 border-b border-gray-200 text-xs"> <tr id="filter-row" class="bg-gray-100 border-b border-gray-200 text-xs">
<input type="hidden" name="sortField" id="sortField" th:value="${sortField}">
<input type="hidden" name="sortDirection" id="sortDirection" th:value="${sortDirection}">
<td class="py-2 px-3"> <td class="py-2 px-3">
<input type="text" id="filterNom" name="nom" th:value="${nomFilter}" placeholder="Filtrer Nom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none"> <input type="text" id="filterNom" name="nom" th:value="${nomFilter}" placeholder="Filtrer Nom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
</td> </td>
@@ -175,7 +222,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 +237,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 +252,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>
@@ -217,5 +264,18 @@
</div> </div>
</div> </div>
</main> </main>
<script>
function updateSort(field) {
let sf = document.getElementById('sortField');
let sd = document.getElementById('sortDirection');
if (sf.value === field) {
sd.value = sd.value === 'asc' ? 'desc' : 'asc';
} else {
sf.value = field;
sd.value = 'asc';
}
sf.dispatchEvent(new Event('change', { bubbles: true }));
}
</script>
</body> </body>
</html> </html>
@@ -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,22 @@
<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 th:if="${errorMessage}" class="bg-red-50 border-l-4 border-red-500 p-4 mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label> <p class="text-sm text-red-700" th:text="${errorMessage}"></p>
<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 class="grid grid-cols-2 gap-4">
<div>
<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 +66,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,13 +46,19 @@
<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>
<td class="py-4 px-6 text-right font-medium text-purple-600" th:text="${cat.tarifExterieur != null ? cat.tarifExterieur + ' €' : '-'}">130.00 €</td> <td class="py-4 px-6 text-right font-medium text-purple-600" th:text="${cat.tarifExterieur != null ? cat.tarifExterieur + ' €' : '-'}">130.00 €</td>
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center"> <td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
<a th:href="@{/categories/{id}/edit(id=${cat.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a> <a th:href="@{/categories/{id}/edit(id=${cat.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
<form th:action="@{/categories/{id}/duplicate(id=${cat.id})}" method="post">
<button type="submit" class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">Dupliquer</button>
</form>
<form th:action="@{/categories/{id}/delete(id=${cat.id})}" method="post" onsubmit="return confirm('Supprimer cette catégorie ?');"> <form th:action="@{/categories/{id}/delete(id=${cat.id})}" method="post" onsubmit="return confirm('Supprimer cette catégorie ?');">
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button> <button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
</form> </form>
@@ -61,5 +70,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>
@@ -19,6 +19,9 @@
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div class="max-w-xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8"> <div class="max-w-xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<div th:if="${errorMessage}" class="bg-red-50 border-l-4 border-red-500 p-4 mb-6">
<p class="text-sm text-red-700" th:text="${errorMessage}"></p>
</div>
<form th:action="@{/equipements}" th:object="${equipement}" method="post" class="space-y-6"> <form th:action="@{/equipements}" th:object="${equipement}" method="post" class="space-y-6">
<input type="hidden" th:field="*{id}" /> <input type="hidden" th:field="*{id}" />
@@ -36,10 +39,79 @@
<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>
<div class="mb-2">
<select id="grilleTailles" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-gray-50" onchange="applyGrilleTailles()">
<option value="">-- Utiliser une grille prédéfinie --</option>
<option value="5/6ANS (Taille 116cm),7/8ANS (Taille 128cm),9/10ANS (Taille 140cm),11/12ANS (Taille 152cm),13/14ANS (Taille 164cm)">Vêtements Junior</option>
<option value="XS,S,M,L,XL,XXL">Vêtements Adulte</option>
<option value="5/6ANS (Taille 116cm),7/8ANS (Taille 128cm),9/10ANS (Taille 140cm),11/12ANS (Taille 152cm),13/14ANS (Taille 164cm),XS,S,M,L,XL,XXL">Vêtements (Junior + Adulte)</option>
<option value="27/30 (Taille 0),31/34 (Taille 1),35/38 (Taille 2),39/42 (Taille 3)">Pointures Junior</option>
<option value="35/38 (Taille 2),39/42 (Taille 3),43/46 (Taille 4)">Pointures Adulte</option>
<option value="27/30 (Taille 0),31/34 (Taille 1),35/38 (Taille 2),39/42 (Taille 3),43/46 (Taille 4)">Pointures (Junior + Adulte)</option>
</select>
</div>
<input type="text" id="taillesDisponibles" 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. Vous pouvez utiliser une grille ci-dessus et ajuster manuellement.</p>
<script>
function applyGrilleTailles() {
const select = document.getElementById('grilleTailles');
const input = document.getElementById('taillesDisponibles');
if (select.value) {
input.value = select.value;
select.value = ''; // Reset select after applying
}
}
</script>
</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 border-t border-gray-100">
<h3 class="text-sm font-medium text-gray-900 mb-4">Options de personnalisation (Flocage)</h3>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" th:field="*{hasFlocagePrenom}" 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">Flocage : Prénom du joueur</label>
</div>
<div class="flex items-center">
<input type="checkbox" th:field="*{hasFlocageInitiales}" 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">Flocage : Initiales du joueur</label>
</div>
<div class="flex items-center">
<input type="checkbox" th:field="*{hasFlocageNumero}" 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">Flocage : Numéro</label>
</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>
@@ -41,6 +44,9 @@
<td class="py-4 px-6 text-gray-600" th:text="${equip.description != null ? equip.description : '-'}">Maillot de match officiel</td> <td class="py-4 px-6 text-gray-600" th:text="${equip.description != null ? equip.description : '-'}">Maillot de match officiel</td>
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center"> <td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
<a th:href="@{/equipements/{id}/edit(id=${equip.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a> <a th:href="@{/equipements/{id}/edit(id=${equip.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
<form th:action="@{/equipements/{id}/duplicate(id=${equip.id})}" method="post">
<button type="submit" class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">Dupliquer</button>
</form>
<form th:action="@{/equipements/{id}/delete(id=${equip.id})}" method="post" onsubmit="return confirm('Supprimer cet équipement ? Cela supprimera également les dotations associées de toutes les licences.');"> <form th:action="@{/equipements/{id}/delete(id=${equip.id})}" method="post" onsubmit="return confirm('Supprimer cet équipement ? Cela supprimera également les dotations associées de toutes les licences.');">
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button> <button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
</form> </form>
@@ -51,5 +57,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,32 @@
<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="raisonChangementClubBlock" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Raison du changement de club <span class="text-red-500">*</span></label>
<input type="text" id="raisonChangementClub" th:field="*{raisonChangementClub}" 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="Pourquoi souhaitez-vous nous rejoindre ?">
</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,9 +88,14 @@
<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>
<label class="block text-sm font-medium text-gray-700 mb-1">Comment avez-vous connu l'AS Talange ?</label>
<input type="text" th:field="*{commentConnuClub}" 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="Ex: Réseaux sociaux, amis, forum des associations...">
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -75,6 +109,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,11 +157,30 @@
<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>
<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">Taille de vêtement souhaitée (équipement)</label>
<select th:field="*{tailleVetement}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez une taille...</option>
<option th:each="taille : ${taillesDisponibles}" th:value="${taille}" th:text="${taille}"></option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Pointure souhaitée (chaussettes)</label>
<select th:field="*{pointure}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">Sélectionnez une pointure...</option>
<option th:each="pt : ${pointuresDisponibles}" th:value="${pt}" th:text="${pt}"></option>
</select>
</div>
</div>
<div class="flex items-start pt-2"> <div class="flex items-start pt-2">
<div class="flex items-center h-5"> <div class="flex items-center h-5">
<input id="consentementRgpd" name="consentementRgpd" type="checkbox" required class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"> <input id="consentementRgpd" name="consentementRgpd" type="checkbox" required class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
@@ -131,6 +198,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 +209,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 +247,35 @@
nouvelleLicenceBlock.classList.remove('hidden'); nouvelleLicenceBlock.classList.remove('hidden');
} }
const ancienClubInput = document.getElementById('ancienClub');
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
const raisonChangementClubBlock = document.getElementById('raisonChangementClubBlock');
const raisonChangementClubInput = document.getElementById('raisonChangementClub');
if (ancienClubInput) {
ancienClubInput.addEventListener('input', function() {
if (this.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden');
ancienneCategorieSelect.required = true;
raisonChangementClubBlock.classList.remove('hidden');
raisonChangementClubInput.required = true;
} else {
ancienneCategorieBlock.classList.add('hidden');
ancienneCategorieSelect.required = false;
raisonChangementClubBlock.classList.add('hidden');
raisonChangementClubInput.required = false;
}
});
// Initial check
if (ancienClubInput.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden');
ancienneCategorieSelect.required = true;
raisonChangementClubBlock.classList.remove('hidden');
raisonChangementClubInput.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</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>as-talange-parent</name> <name>as-talange-parent</name>