Compare commits
38
Commits
348d6e6ccf
...
prod/v1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2a2e486a | ||
|
|
b9a7dc089c | ||
|
|
1737087062 | ||
|
|
df217bc923 | ||
|
|
da0443f655 | ||
|
|
baa4d3c0cd | ||
|
|
e81be98cbe | ||
|
|
a19e49392b | ||
|
|
8ca20c9a4f | ||
|
|
92c9e80bc3 | ||
|
|
c94f89997f | ||
|
|
af31766134 | ||
|
|
3fc239683e | ||
|
|
a7c5b23198 | ||
|
|
29f9dc3bc2 | ||
|
|
5ee9be467b | ||
|
|
6dd45deb8c | ||
|
|
19cee92437 | ||
|
|
214ec08229 | ||
|
|
62209ae659 | ||
|
|
fc3373a1b4 | ||
|
|
db3eb3ab9a | ||
|
|
d1f38f11f0 | ||
|
|
acf9d6d301 | ||
|
|
3066ff0e86 | ||
|
|
64ad4be7dd | ||
|
|
d30e795325 | ||
|
|
e0388c7884 | ||
|
|
7cf29f6106 | ||
|
|
bef01e3a0e | ||
|
|
9cf1d26018 | ||
|
|
94d672c0f6 | ||
|
|
8156c8f3d0 | ||
|
|
7249b45533 | ||
|
|
7e036c1966 | ||
|
|
194986f880 | ||
|
|
d834b8d0d3 | ||
|
|
e3abe53eb0 |
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
|
}
|
||||||
@@ -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.4-SNAPSHOT</version>
|
<version>1.6</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
|||||||
@@ -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.4-SNAPSHOT</version>
|
<version>1.6</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; }
|
||||||
|
|
||||||
|
|||||||
@@ -36,4 +36,10 @@ public class AppUser {
|
|||||||
inverseJoinColumns = @JoinColumn(name = "role_id")
|
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||||||
)
|
)
|
||||||
private Set<Role> roles = new HashSet<>();
|
private Set<Role> roles = new HashSet<>();
|
||||||
|
|
||||||
|
@Column(name = "last_login_at")
|
||||||
|
private java.time.LocalDateTime lastLoginAt;
|
||||||
|
|
||||||
|
@Column(name = "last_logout_at")
|
||||||
|
private java.time.LocalDateTime lastLogoutAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -58,4 +64,107 @@ public class Dotation {
|
|||||||
|
|
||||||
public Boolean getChoisi() { return choisi; }
|
public Boolean getChoisi() { return choisi; }
|
||||||
public void setChoisi(Boolean choisi) { this.choisi = choisi; }
|
public void setChoisi(Boolean choisi) { this.choisi = choisi; }
|
||||||
|
|
||||||
|
public static boolean isSizeMatch(String option, String adherentSize) {
|
||||||
|
if (option == null || adherentSize == null) return false;
|
||||||
|
String opt = option.trim();
|
||||||
|
String adh = adherentSize.trim();
|
||||||
|
if (opt.isEmpty() || adh.isEmpty()) return false;
|
||||||
|
|
||||||
|
if (opt.equalsIgnoreCase(adh)) return true;
|
||||||
|
|
||||||
|
String optNorm = opt.toLowerCase().replaceAll("\\s+", " ");
|
||||||
|
String adhNorm = adh.toLowerCase().replaceAll("\\s+", " ");
|
||||||
|
|
||||||
|
if (optNorm.equals(adhNorm)) return true;
|
||||||
|
|
||||||
|
// 1. Age bracket matching: 5/6, 7/8, 9/10, 11/12, 13/14, 15/16
|
||||||
|
java.util.regex.Pattern agePattern = java.util.regex.Pattern.compile("(?<!\\d)(5[/\\-]6|7[/\\-]8|9[/\\-]10|11[/\\-]12|13[/\\-]14|15[/\\-]16)(?!\\d)");
|
||||||
|
java.util.regex.Matcher adhAgeMatcher = agePattern.matcher(adhNorm);
|
||||||
|
java.util.regex.Matcher optAgeMatcher = agePattern.matcher(optNorm);
|
||||||
|
boolean adhHasAge = adhAgeMatcher.find();
|
||||||
|
boolean optHasAge = optAgeMatcher.find();
|
||||||
|
|
||||||
|
if (adhHasAge && optHasAge) {
|
||||||
|
String adhAgeKey = adhAgeMatcher.group(1).replace("-", "/");
|
||||||
|
String optAgeKey = optAgeMatcher.group(1).replace("-", "/");
|
||||||
|
return adhAgeKey.equals(optAgeKey);
|
||||||
|
} else if (adhHasAge) {
|
||||||
|
String ageKey = adhAgeMatcher.group(1).replace("-", "/");
|
||||||
|
String altAgeKey = ageKey.replace("/", "-");
|
||||||
|
if (optNorm.contains(ageKey) || optNorm.contains(altAgeKey)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Height matching (3-digit numbers in cm, e.g. 115, 116, 126, 128, 138, 140, 150, 152, 162, 164, 176)
|
||||||
|
java.util.regex.Pattern heightPattern = java.util.regex.Pattern.compile("(?<!\\d)(115|116|126|128|138|140|150|152|162|164|176)(?!\\d)");
|
||||||
|
java.util.regex.Matcher adhHeightMatcher = heightPattern.matcher(adhNorm);
|
||||||
|
java.util.regex.Matcher optHeightMatcher = heightPattern.matcher(optNorm);
|
||||||
|
boolean adhHasHeight = adhHeightMatcher.find();
|
||||||
|
boolean optHasHeight = optHeightMatcher.find();
|
||||||
|
|
||||||
|
if (adhHasHeight && optHasHeight) {
|
||||||
|
int adhH = Integer.parseInt(adhHeightMatcher.group(1));
|
||||||
|
int optH = Integer.parseInt(optHeightMatcher.group(1));
|
||||||
|
return Math.abs(adhH - optH) <= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Shoe size / pointure matching: 27/30, 31/34, 35/38, 39/42, 43/46
|
||||||
|
java.util.regex.Pattern shoePattern = java.util.regex.Pattern.compile("(?<!\\d)(27[/\\-]30|31[/\\-]34|35[/\\-]38|39[/\\-]42|43[/\\-]46)(?!\\d)");
|
||||||
|
java.util.regex.Matcher adhShoeMatcher = shoePattern.matcher(adhNorm);
|
||||||
|
java.util.regex.Matcher optShoeMatcher = shoePattern.matcher(optNorm);
|
||||||
|
boolean adhHasShoe = adhShoeMatcher.find();
|
||||||
|
boolean optHasShoe = optShoeMatcher.find();
|
||||||
|
|
||||||
|
if (adhHasShoe && optHasShoe) {
|
||||||
|
String adhShoeKey = adhShoeMatcher.group(1).replace("-", "/");
|
||||||
|
String optShoeKey = optShoeMatcher.group(1).replace("-", "/");
|
||||||
|
return adhShoeKey.equals(optShoeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Sock size tag matching: "taille 0", "taille 1", "taille 2", "taille 3", "taille 4"
|
||||||
|
java.util.regex.Pattern tNumPattern = java.util.regex.Pattern.compile("(?<![a-z0-9])taille\\s*([0-4])(?![0-9])");
|
||||||
|
java.util.regex.Matcher adhTNumMatcher = tNumPattern.matcher(adhNorm);
|
||||||
|
java.util.regex.Matcher optTNumMatcher = tNumPattern.matcher(optNorm);
|
||||||
|
boolean adhHasTNum = adhTNumMatcher.find();
|
||||||
|
boolean optHasTNum = optTNumMatcher.find();
|
||||||
|
|
||||||
|
if (adhHasTNum && optHasTNum) {
|
||||||
|
return adhTNumMatcher.group(1).equals(optTNumMatcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Letter size matching: XXL, XL, L, M, S, XS
|
||||||
|
java.util.regex.Pattern letterPattern = java.util.regex.Pattern.compile("(?<![a-z0-9])(xxl|xl|l|m|s|xs)(?![a-z0-9])");
|
||||||
|
java.util.regex.Matcher adhLetterMatcher = letterPattern.matcher(adhNorm);
|
||||||
|
java.util.regex.Matcher optLetterMatcher = letterPattern.matcher(optNorm);
|
||||||
|
boolean adhHasLetter = adhLetterMatcher.find();
|
||||||
|
boolean optHasLetter = optLetterMatcher.find();
|
||||||
|
|
||||||
|
if (adhHasLetter && optHasLetter) {
|
||||||
|
return adhLetterMatcher.group(1).equals(optLetterMatcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substring fallback only if neither age, height, shoe, tag, nor letter pattern were present
|
||||||
|
if (!adhHasAge && !optHasAge && !adhHasHeight && !optHasHeight && !adhHasShoe && !optHasShoe && !adhHasTNum && !optHasTNum && !adhHasLetter && !optHasLetter) {
|
||||||
|
if (optNorm.length() >= 2 && adhNorm.contains(optNorm)) return true;
|
||||||
|
if (adhNorm.length() >= 2 && optNorm.contains(adhNorm)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTailleOptionSelected(String option) {
|
||||||
|
if (option == null) return false;
|
||||||
|
String trimmedOpt = option.trim();
|
||||||
|
if (this.taille != null && !this.taille.trim().isEmpty()) {
|
||||||
|
return this.taille.trim().equalsIgnoreCase(trimmedOpt);
|
||||||
|
}
|
||||||
|
if (this.licence == null || this.licence.getAdherent() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Adherent adherent = this.licence.getAdherent();
|
||||||
|
return isSizeMatch(trimmedOpt, adherent.getTailleVetement()) || isSizeMatch(trimmedOpt, adherent.getPointure());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
package com.astalange.core.security;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.AppUser;
|
||||||
|
import com.astalange.core.repository.AppUserRepository;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||||
|
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class AuthenticationEventListener {
|
||||||
|
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
|
||||||
|
public AuthenticationEventListener(AppUserRepository userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
@Transactional
|
||||||
|
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
|
||||||
|
String username = extractUsername(event.getAuthentication().getPrincipal());
|
||||||
|
if (username != null) {
|
||||||
|
userRepository.findByUsername(username).ifPresent(user -> {
|
||||||
|
user.setLastLoginAt(LocalDateTime.now());
|
||||||
|
userRepository.save(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
@Transactional
|
||||||
|
public void onLogoutSuccess(LogoutSuccessEvent event) {
|
||||||
|
if (event.getAuthentication() != null) {
|
||||||
|
String username = extractUsername(event.getAuthentication().getPrincipal());
|
||||||
|
if (username != null) {
|
||||||
|
userRepository.findByUsername(username).ifPresent(user -> {
|
||||||
|
user.setLastLogoutAt(LocalDateTime.now());
|
||||||
|
userRepository.save(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
@Transactional
|
||||||
|
public void onSessionDestroyed(HttpSessionDestroyedEvent event) {
|
||||||
|
List<SecurityContext> contexts = event.getSecurityContexts();
|
||||||
|
for (SecurityContext context : contexts) {
|
||||||
|
if (context != null && context.getAuthentication() != null) {
|
||||||
|
String username = extractUsername(context.getAuthentication().getPrincipal());
|
||||||
|
if (username != null) {
|
||||||
|
userRepository.findByUsername(username).ifPresent(user -> {
|
||||||
|
if (user.getLastLogoutAt() == null || (user.getLastLoginAt() != null && user.getLastLogoutAt().isBefore(user.getLastLoginAt()))) {
|
||||||
|
user.setLastLogoutAt(LocalDateTime.now());
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractUsername(Object principal) {
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
return ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
return (String) principal;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())) {
|
||||||
|
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;
|
found = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +112,34 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String matchedTaille = "";
|
||||||
|
if (licence.getAdherent() != null) {
|
||||||
|
String taillesDispo = ce.getEquipement().getTaillesDisponibles();
|
||||||
|
if (taillesDispo != null && !taillesDispo.trim().isEmpty()) {
|
||||||
|
String[] dispos = taillesDispo.split(",");
|
||||||
|
String tv = licence.getAdherent().getTailleVetement();
|
||||||
|
String pt = licence.getAdherent().getPointure();
|
||||||
|
for (String t : dispos) {
|
||||||
|
String trimmed = t.trim();
|
||||||
|
if (Dotation.isSizeMatch(trimmed, tv) || Dotation.isSizeMatch(trimmed, pt)) {
|
||||||
|
matchedTaille = trimmed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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())) {
|
||||||
@@ -110,6 +147,9 @@ public class CategorieService {
|
|||||||
if (Boolean.TRUE.equals(ce.getObligatoire()) && !Boolean.TRUE.equals(d.getChoisi())) {
|
if (Boolean.TRUE.equals(ce.getObligatoire()) && !Boolean.TRUE.equals(d.getChoisi())) {
|
||||||
d.setChoisi(true);
|
d.setChoisi(true);
|
||||||
}
|
}
|
||||||
|
if ((d.getTaille() == null || d.getTaille().trim().isEmpty()) && !matchedTaille.isEmpty()) {
|
||||||
|
d.setTaille(matchedTaille);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,19 +158,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(matchedTaille);
|
||||||
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 +188,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);
|
||||||
|
|
||||||
|
|||||||
+10
@@ -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);
|
||||||
+2
@@ -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;
|
||||||
+2
@@ -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;
|
||||||
+9
@@ -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);
|
||||||
+31
@@ -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
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE app_user ADD COLUMN last_login_at TIMESTAMP;
|
||||||
|
ALTER TABLE app_user ADD COLUMN last_logout_at TIMESTAMP;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.astalange.core;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.Dotation;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class DotationSizeMatchTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdherent60SizeMatchBugFix() {
|
||||||
|
String adherentTaille = "11/12ANS (Taille 152cm)";
|
||||||
|
String adherentPointure = "35/38 (Taille 2)";
|
||||||
|
|
||||||
|
// Equipment options for Junior Maillot / Shorts / Tracksuits
|
||||||
|
String opt56 = "5/6ANS (Taille 116cm)";
|
||||||
|
String opt78 = "7/8ANS (Taille 128cm)";
|
||||||
|
String opt910 = "9/10ANS (Taille 140cm)";
|
||||||
|
String opt1112 = "11/12ANS (Taille 152cm)";
|
||||||
|
String opt1314 = "13/14ANS (Taille 164cm)";
|
||||||
|
|
||||||
|
assertFalse(Dotation.isSizeMatch(opt56, adherentTaille), "5/6ANS should NOT match 11/12ANS");
|
||||||
|
assertFalse(Dotation.isSizeMatch(opt78, adherentTaille), "7/8ANS should NOT match 11/12ANS");
|
||||||
|
assertFalse(Dotation.isSizeMatch(opt910, adherentTaille), "9/10ANS should NOT match 11/12ANS");
|
||||||
|
assertTrue(Dotation.isSizeMatch(opt1112, adherentTaille), "11/12ANS MUST match 11/12ANS");
|
||||||
|
assertFalse(Dotation.isSizeMatch(opt1314, adherentTaille), "13/14ANS should NOT match 11/12ANS");
|
||||||
|
|
||||||
|
// Sock sizes
|
||||||
|
String sock2730 = "27/30 (Taille 0)";
|
||||||
|
String sock3134 = "31/34 (Taille 1)";
|
||||||
|
String sock3538 = "35/38 (Taille 2)";
|
||||||
|
String sock3942 = "39/42 (Taille 3)";
|
||||||
|
|
||||||
|
assertFalse(Dotation.isSizeMatch(sock2730, adherentPointure), "27/30 should NOT match 35/38");
|
||||||
|
assertFalse(Dotation.isSizeMatch(sock3134, adherentPointure), "31/34 should NOT match 35/38");
|
||||||
|
assertTrue(Dotation.isSizeMatch(sock3538, adherentPointure), "35/38 MUST match 35/38");
|
||||||
|
assertFalse(Dotation.isSizeMatch(sock3942, adherentPointure), "39/42 should NOT match 35/38");
|
||||||
|
|
||||||
|
// Ensure sock sizes do not match clothing size 152cm due to "Taille 1"
|
||||||
|
assertFalse(Dotation.isSizeMatch(sock3134, adherentTaille), "Sock 31/34 (Taille 1) should NOT match clothing 152cm");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLetterSizes() {
|
||||||
|
assertFalse(Dotation.isSizeMatch("S", "XL"));
|
||||||
|
assertFalse(Dotation.isSizeMatch("M", "XL"));
|
||||||
|
assertTrue(Dotation.isSizeMatch("XL", "XL"));
|
||||||
|
assertTrue(Dotation.isSizeMatch("xxl", "XXL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSpaceVariations() {
|
||||||
|
assertTrue(Dotation.isSizeMatch("5/6 ANS (Taille 116cm)", "5/6ANS"));
|
||||||
|
assertTrue(Dotation.isSizeMatch("11/12 ANS (Taille 152cm)", "11/12ANS (Taille 152 cm)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
package com.astalange.core.service;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.*;
|
||||||
|
import com.astalange.core.repository.CategorieRepository;
|
||||||
|
import com.astalange.core.repository.DotationRepository;
|
||||||
|
import com.astalange.core.repository.EquipementRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class DotationIntegrationTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CategorieRepository categorieRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SaisonRepository saisonRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EquipementRepository equipementRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private com.astalange.core.repository.LicenceRepository licenceRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CategorieService categorieService;
|
||||||
|
|
||||||
|
private Equipement maillotJunior;
|
||||||
|
private Equipement chaussettesJunior;
|
||||||
|
private Equipement maillotAdulte;
|
||||||
|
private Equipement chaussettesAdulte;
|
||||||
|
private Categorie catJunior;
|
||||||
|
private Categorie catAdulte;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
// 1. Junior Equipment
|
||||||
|
maillotJunior = new Equipement();
|
||||||
|
maillotJunior.setId(1L);
|
||||||
|
maillotJunior.setNom("Maillot de match Puma (Junior)");
|
||||||
|
maillotJunior.setTypePublic("TOUS");
|
||||||
|
maillotJunior.setTaillesDisponibles("5/6ANS (Taille 116cm),7/8ANS (Taille 128cm),9/10ANS (Taille 140cm),11/12ANS (Taille 152cm),13/14ANS (Taille 164cm),15/16ANS (Taille 176cm)");
|
||||||
|
|
||||||
|
chaussettesJunior = new Equipement();
|
||||||
|
chaussettesJunior.setId(2L);
|
||||||
|
chaussettesJunior.setNom("Chaussettes puma (Junior)");
|
||||||
|
chaussettesJunior.setTypePublic("TOUS");
|
||||||
|
chaussettesJunior.setTaillesDisponibles("27/30 (Taille 0),31/34 (Taille 1),35/38 (Taille 2),39/42 (Taille 3)");
|
||||||
|
|
||||||
|
catJunior = new Categorie();
|
||||||
|
catJunior.setId(10L);
|
||||||
|
catJunior.setNom("U11");
|
||||||
|
|
||||||
|
CategorieEquipement ce1 = new CategorieEquipement();
|
||||||
|
ce1.setCategorie(catJunior);
|
||||||
|
ce1.setEquipement(maillotJunior);
|
||||||
|
ce1.setObligatoire(true);
|
||||||
|
|
||||||
|
CategorieEquipement ce2 = new CategorieEquipement();
|
||||||
|
ce2.setCategorie(catJunior);
|
||||||
|
ce2.setEquipement(chaussettesJunior);
|
||||||
|
ce2.setObligatoire(true);
|
||||||
|
|
||||||
|
catJunior.setCategorieEquipements(List.of(ce1, ce2));
|
||||||
|
|
||||||
|
// 2. Adult Equipment
|
||||||
|
maillotAdulte = new Equipement();
|
||||||
|
maillotAdulte.setId(3L);
|
||||||
|
maillotAdulte.setNom("Maillot de match Puma (Adulte)");
|
||||||
|
maillotAdulte.setTypePublic("TOUS");
|
||||||
|
maillotAdulte.setTaillesDisponibles("XS,S,M,L,XL,XXL");
|
||||||
|
|
||||||
|
chaussettesAdulte = new Equipement();
|
||||||
|
chaussettesAdulte.setId(4L);
|
||||||
|
chaussettesAdulte.setNom("Chaussettes puma (Adulte)");
|
||||||
|
chaussettesAdulte.setTypePublic("TOUS");
|
||||||
|
chaussettesAdulte.setTaillesDisponibles("35/38 (Taille 2),39/42 (Taille 3),43/46 (Taille 4)");
|
||||||
|
|
||||||
|
catAdulte = new Categorie();
|
||||||
|
catAdulte.setId(20L);
|
||||||
|
catAdulte.setNom("Seniors");
|
||||||
|
|
||||||
|
CategorieEquipement ce3 = new CategorieEquipement();
|
||||||
|
ce3.setCategorie(catAdulte);
|
||||||
|
ce3.setEquipement(maillotAdulte);
|
||||||
|
ce3.setObligatoire(true);
|
||||||
|
|
||||||
|
CategorieEquipement ce4 = new CategorieEquipement();
|
||||||
|
ce4.setCategorie(catAdulte);
|
||||||
|
ce4.setEquipement(chaussettesAdulte);
|
||||||
|
ce4.setObligatoire(true);
|
||||||
|
|
||||||
|
catAdulte.setCategorieEquipements(List.of(ce3, ce4));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Licence createLicence(Categorie cat, String tailleVetement, String pointure) {
|
||||||
|
Adherent adherent = new Adherent();
|
||||||
|
adherent.setTypeMaillot("JOUEUR");
|
||||||
|
adherent.setTailleVetement(tailleVetement);
|
||||||
|
adherent.setPointure(pointure);
|
||||||
|
|
||||||
|
Licence licence = new Licence();
|
||||||
|
licence.setId(100L);
|
||||||
|
licence.setAdherent(adherent);
|
||||||
|
licence.setCategorie(cat);
|
||||||
|
licence.setDotations(new ArrayList<>());
|
||||||
|
return licence;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Junior 11/12ANS (Bug initial Adhérent 60) -> Doit reporter 11/12ANS et 35/38")
|
||||||
|
public void testAdherent60Synchronization() {
|
||||||
|
Licence licence = createLicence(catJunior, "11/12ANS (Taille 152cm)", "35/38 (Taille 2)");
|
||||||
|
|
||||||
|
categorieService.syncDotationsForLicence(licence);
|
||||||
|
|
||||||
|
List<Dotation> dotations = licence.getDotations();
|
||||||
|
assertEquals(2, dotations.size());
|
||||||
|
Dotation dMaillot = dotations.stream().filter(d -> d.getEquipement().getId().equals(1L)).findFirst().orElseThrow();
|
||||||
|
Dotation dChaussettes = dotations.stream().filter(d -> d.getEquipement().getId().equals(2L)).findFirst().orElseThrow();
|
||||||
|
|
||||||
|
assertEquals("11/12ANS (Taille 152cm)", dMaillot.getTaille(), "Le maillot doit être 11/12ANS et non 5/6ANS");
|
||||||
|
assertEquals("35/38 (Taille 2)", dChaussettes.getTaille(), "Les chaussettes doivent être 35/38 et non 31/34");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Junior - Test de toutes les tailles (5/6, 7/8, 9/10, 11/12, 13/14, 15/16)")
|
||||||
|
public void testAllJuniorSizes() {
|
||||||
|
String[][] cases = {
|
||||||
|
{"5/6ANS (Taille 116cm)", "27/30 (Taille 0)", "5/6ANS (Taille 116cm)", "27/30 (Taille 0)"},
|
||||||
|
{"7/8ANS (Taille 128cm)", "31/34 (Taille 1)", "7/8ANS (Taille 128cm)", "31/34 (Taille 1)"},
|
||||||
|
{"9/10ANS (Taille 140cm)", "31/34 (Taille 1)", "9/10ANS (Taille 140cm)", "31/34 (Taille 1)"},
|
||||||
|
{"11/12ANS (Taille 152cm)", "35/38 (Taille 2)", "11/12ANS (Taille 152cm)", "35/38 (Taille 2)"},
|
||||||
|
{"13/14ANS (Taille 164cm)", "35/38 (Taille 2)", "13/14ANS (Taille 164cm)", "35/38 (Taille 2)"},
|
||||||
|
{"15/16ANS (Taille 176cm)", "39/42 (Taille 3)", "15/16ANS (Taille 176cm)", "39/42 (Taille 3)"}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String[] testCase : cases) {
|
||||||
|
String tv = testCase[0];
|
||||||
|
String pt = testCase[1];
|
||||||
|
String expectedMaillot = testCase[2];
|
||||||
|
String expectedChaussettes = testCase[3];
|
||||||
|
|
||||||
|
Licence licence = createLicence(catJunior, tv, pt);
|
||||||
|
categorieService.syncDotationsForLicence(licence);
|
||||||
|
|
||||||
|
List<Dotation> dotations = licence.getDotations();
|
||||||
|
Dotation dMaillot = dotations.stream().filter(d -> d.getEquipement().getId().equals(1L)).findFirst().orElseThrow();
|
||||||
|
Dotation dChaussettes = dotations.stream().filter(d -> d.getEquipement().getId().equals(2L)).findFirst().orElseThrow();
|
||||||
|
|
||||||
|
assertEquals(expectedMaillot, dMaillot.getTaille(), "Erreur pour la taille vêtement: " + tv);
|
||||||
|
assertEquals(expectedChaussettes, dChaussettes.getTaille(), "Erreur pour la pointure: " + pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Adulte - Test de toutes les tailles (XS, S, M, L, XL, XXL)")
|
||||||
|
public void testAllAdultSizes() {
|
||||||
|
String[][] cases = {
|
||||||
|
{"XS", "35/38 (Taille 2)", "XS", "35/38 (Taille 2)"},
|
||||||
|
{"S", "39/42 (Taille 3)", "S", "39/42 (Taille 3)"},
|
||||||
|
{"M", "39/42 (Taille 3)", "M", "39/42 (Taille 3)"},
|
||||||
|
{"L", "43/46 (Taille 4)", "L", "43/46 (Taille 4)"},
|
||||||
|
{"XL", "43/46 (Taille 4)", "XL", "43/46 (Taille 4)"},
|
||||||
|
{"XXL", "43/46 (Taille 4)", "XXL", "43/46 (Taille 4)"}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String[] testCase : cases) {
|
||||||
|
String tv = testCase[0];
|
||||||
|
String pt = testCase[1];
|
||||||
|
String expectedMaillot = testCase[2];
|
||||||
|
String expectedChaussettes = testCase[3];
|
||||||
|
|
||||||
|
Licence licence = createLicence(catAdulte, tv, pt);
|
||||||
|
categorieService.syncDotationsForLicence(licence);
|
||||||
|
|
||||||
|
List<Dotation> dotations = licence.getDotations();
|
||||||
|
Dotation dMaillot = dotations.stream().filter(d -> d.getEquipement().getId().equals(3L)).findFirst().orElseThrow();
|
||||||
|
Dotation dChaussettes = dotations.stream().filter(d -> d.getEquipement().getId().equals(4L)).findFirst().orElseThrow();
|
||||||
|
|
||||||
|
assertEquals(expectedMaillot, dMaillot.getTaille(), "Erreur pour la taille adulte vêtement: " + tv);
|
||||||
|
assertEquals(expectedChaussettes, dChaussettes.getTaille(), "Erreur pour la pointure adulte: " + pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Formats alternatifs (Espaces, sans mention de taille)")
|
||||||
|
public void testAlternativeFormats() {
|
||||||
|
Licence licence1 = createLicence(catJunior, "11/12 ANS (Taille 152 cm)", "35/38 (Taille 2)");
|
||||||
|
categorieService.syncDotationsForLicence(licence1);
|
||||||
|
|
||||||
|
Dotation dMaillot1 = licence1.getDotations().stream().filter(d -> d.getEquipement().getId().equals(1L)).findFirst().orElseThrow();
|
||||||
|
assertEquals("11/12ANS (Taille 152cm)", dMaillot1.getTaille());
|
||||||
|
|
||||||
|
Licence licence2 = createLicence(catJunior, "11/12ANS", "35/38");
|
||||||
|
categorieService.syncDotationsForLicence(licence2);
|
||||||
|
|
||||||
|
Dotation dMaillot2 = licence2.getDotations().stream().filter(d -> d.getEquipement().getId().equals(1L)).findFirst().orElseThrow();
|
||||||
|
Dotation dChaussettes2 = licence2.getDotations().stream().filter(d -> d.getEquipement().getId().equals(2L)).findFirst().orElseThrow();
|
||||||
|
assertEquals("11/12ANS (Taille 152cm)", dMaillot2.getTaille());
|
||||||
|
assertEquals("35/38 (Taille 2)", dChaussettes2.getTaille());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.4-SNAPSHOT</version>
|
<version>1.6</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,11 +201,13 @@ 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) {
|
||||||
|
if (dateChanged) {
|
||||||
Saison saison = licence.getSaison();
|
Saison saison = licence.getSaison();
|
||||||
Categorie newCat = categorieRepository.findBySaison(saison).stream()
|
Categorie newCat = categorieRepository.findBySaison(saison).stream()
|
||||||
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
|
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
|
||||||
@@ -190,9 +216,15 @@ public class AdherentController {
|
|||||||
if (newCat != null) {
|
if (newCat != null) {
|
||||||
licence.setCategorie(newCat);
|
licence.setCategorie(newCat);
|
||||||
licence = licenceRepository.save(licence);
|
licence = licenceRepository.save(licence);
|
||||||
categorieService.syncDotationsForLicence(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
existing.setNom(adherent.getNom());
|
existing.setNom(adherent.getNom());
|
||||||
@@ -202,12 +234,20 @@ public class AdherentController {
|
|||||||
existing.setLieuNaissancePays(adherent.getLieuNaissancePays());
|
existing.setLieuNaissancePays(adherent.getLieuNaissancePays());
|
||||||
existing.setNationalite(adherent.getNationalite());
|
existing.setNationalite(adherent.getNationalite());
|
||||||
existing.setEmail(adherent.getEmail());
|
existing.setEmail(adherent.getEmail());
|
||||||
|
existing.setTelephone(adherent.getTelephone());
|
||||||
existing.setRepresentantLegal(adherent.getRepresentantLegal());
|
existing.setRepresentantLegal(adherent.getRepresentantLegal());
|
||||||
existing.setResidentTalange(adherent.isResidentTalange());
|
existing.setResidentTalange(adherent.isResidentTalange());
|
||||||
existing.setSexe(adherent.getSexe());
|
existing.setSexe(adherent.getSexe());
|
||||||
existing.setTypeMaillot(adherent.getTypeMaillot());
|
existing.setTypeMaillot(adherent.getTypeMaillot());
|
||||||
|
existing.setTailleVetement(adherent.getTailleVetement());
|
||||||
|
existing.setPointure(adherent.getPointure());
|
||||||
|
|
||||||
adherentRepository.save(existing);
|
adherentRepository.save(existing);
|
||||||
|
|
||||||
|
List<Licence> licences = licenceRepository.findByAdherentId(existing.getId());
|
||||||
|
for (Licence licence : licences) {
|
||||||
|
categorieService.syncDotationsForLicence(licence);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
adherentRepository.save(adherent);
|
adherentRepository.save(adherent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ public class CategorieController {
|
|||||||
|
|
||||||
@GetMapping("/new")
|
@GetMapping("/new")
|
||||||
public String showCreateForm(Model model) {
|
public String showCreateForm(Model model) {
|
||||||
|
if (!model.containsAttribute("categorie")) {
|
||||||
model.addAttribute("categorie", new 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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,19 +76,35 @@ public class DotationController {
|
|||||||
@RequestParam(required = false) Long equipementId,
|
@RequestParam(required = false) Long equipementId,
|
||||||
@RequestParam(required = false) Long categorieId,
|
@RequestParam(required = false) Long categorieId,
|
||||||
@RequestParam(required = false) Boolean fourni,
|
@RequestParam(required = false) Boolean fourni,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
Model model) {
|
Model model) {
|
||||||
|
|
||||||
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
Specification<Dotation> spec = buildSpecification(equipementId, categorieId, fourni, saisonActive);
|
Specification<Dotation> spec = buildSpecification(equipementId, categorieId, fourni, saisonActive);
|
||||||
List<Dotation> dotations = dotationRepository.findAll(spec);
|
List<Dotation> allDotations = dotationRepository.findAll(spec);
|
||||||
|
|
||||||
model.addAttribute("dotations", dotations);
|
int totalElements = allDotations.size();
|
||||||
|
int totalPages = (int) Math.ceil((double) totalElements / size);
|
||||||
|
if (page < 0) page = 0;
|
||||||
|
if (page >= totalPages && totalPages > 0) page = totalPages - 1;
|
||||||
|
|
||||||
|
int start = page * size;
|
||||||
|
int end = Math.min(start + size, totalElements);
|
||||||
|
List<Dotation> pageContent = (start < totalElements) ? allDotations.subList(start, end) : List.of();
|
||||||
|
|
||||||
|
model.addAttribute("dotations", pageContent);
|
||||||
model.addAttribute("equipements", equipementRepository.findAll());
|
model.addAttribute("equipements", equipementRepository.findAll());
|
||||||
model.addAttribute("categories", categorieRepository.findAll());
|
model.addAttribute("categories", categorieRepository.findAll());
|
||||||
model.addAttribute("equipementId", equipementId);
|
model.addAttribute("equipementId", equipementId);
|
||||||
model.addAttribute("categorieId", categorieId);
|
model.addAttribute("categorieId", categorieId);
|
||||||
model.addAttribute("fourni", fourni);
|
model.addAttribute("fourni", fourni);
|
||||||
|
|
||||||
|
model.addAttribute("currentPage", page);
|
||||||
|
model.addAttribute("totalPages", totalPages);
|
||||||
|
model.addAttribute("totalElements", totalElements);
|
||||||
|
model.addAttribute("pageSize", size);
|
||||||
|
|
||||||
return "parametrage/equipements_recherche";
|
return "parametrage/equipements_recherche";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,12 +184,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);
|
||||||
|
|
||||||
|
|||||||
+46
-2
@@ -33,34 +33,78 @@ public class EquipementController {
|
|||||||
|
|
||||||
@GetMapping("/new")
|
@GetMapping("/new")
|
||||||
public String showCreateForm(Model model) {
|
public String showCreateForm(Model model) {
|
||||||
|
if (!model.containsAttribute("equipement")) {
|
||||||
model.addAttribute("equipement", new 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) {
|
||||||
|
if (!model.containsAttribute("equipement")) {
|
||||||
Equipement equipement = equipementRepository.findById(id)
|
Equipement equipement = equipementRepository.findById(id)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + id));
|
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + id));
|
||||||
model.addAttribute("equipement", equipement);
|
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -135,14 +139,25 @@ public class LicenceController {
|
|||||||
@RequestParam(required = false) String numeroLicence,
|
@RequestParam(required = false) String numeroLicence,
|
||||||
@RequestParam(required = false) String email,
|
@RequestParam(required = false) String email,
|
||||||
@RequestParam(required = false) String typeDemande,
|
@RequestParam(required = false) String typeDemande,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
org.springframework.ui.Model model) {
|
org.springframework.ui.Model model) {
|
||||||
|
|
||||||
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
org.springframework.data.jpa.domain.Specification<Licence> spec = buildLicenceSpecification(nom, prenom, categorieId, numeroLicence, email, typeDemande, saisonActive);
|
org.springframework.data.jpa.domain.Specification<Licence> spec = buildLicenceSpecification(nom, prenom, categorieId, numeroLicence, email, typeDemande, saisonActive);
|
||||||
|
|
||||||
List<Licence> licences = licenceRepository.findAll(spec);
|
List<Licence> allLicences = licenceRepository.findAll(spec);
|
||||||
|
|
||||||
model.addAttribute("licences", licences);
|
int totalElements = allLicences.size();
|
||||||
|
int totalPages = (int) Math.ceil((double) totalElements / size);
|
||||||
|
if (page < 0) page = 0;
|
||||||
|
if (page >= totalPages && totalPages > 0) page = totalPages - 1;
|
||||||
|
|
||||||
|
int start = page * size;
|
||||||
|
int end = Math.min(start + size, totalElements);
|
||||||
|
List<Licence> pageContent = (start < totalElements) ? allLicences.subList(start, end) : List.of();
|
||||||
|
|
||||||
|
model.addAttribute("licences", pageContent);
|
||||||
model.addAttribute("categories", categorieRepository.findAll());
|
model.addAttribute("categories", categorieRepository.findAll());
|
||||||
model.addAttribute("nomFilter", nom);
|
model.addAttribute("nomFilter", nom);
|
||||||
model.addAttribute("prenomFilter", prenom);
|
model.addAttribute("prenomFilter", prenom);
|
||||||
@@ -151,6 +166,11 @@ public class LicenceController {
|
|||||||
model.addAttribute("emailFilter", email);
|
model.addAttribute("emailFilter", email);
|
||||||
model.addAttribute("typeDemandeFilter", typeDemande);
|
model.addAttribute("typeDemandeFilter", typeDemande);
|
||||||
|
|
||||||
|
model.addAttribute("currentPage", page);
|
||||||
|
model.addAttribute("totalPages", totalPages);
|
||||||
|
model.addAttribute("totalElements", totalElements);
|
||||||
|
model.addAttribute("pageSize", size);
|
||||||
|
|
||||||
return "parametrage/licences_recherche";
|
return "parametrage/licences_recherche";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-7
@@ -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() + " €";
|
||||||
@@ -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,93 @@
|
|||||||
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.AppUser;
|
||||||
|
import com.astalange.core.entity.Role;
|
||||||
|
import com.astalange.core.repository.AppUserRepository;
|
||||||
|
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.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/admin/sessions")
|
||||||
|
public class SessionController {
|
||||||
|
|
||||||
|
private final SessionRegistry sessionRegistry;
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
|
||||||
|
public SessionController(SessionRegistry sessionRegistry, AppUserRepository userRepository) {
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UserSessionDTO {
|
||||||
|
private final Long id;
|
||||||
|
private final String username;
|
||||||
|
private final boolean online;
|
||||||
|
private final LocalDateTime lastLoginAt;
|
||||||
|
private final LocalDateTime lastLogoutAt;
|
||||||
|
private final String roles;
|
||||||
|
|
||||||
|
public UserSessionDTO(Long id, String username, boolean online, LocalDateTime lastLoginAt, LocalDateTime lastLogoutAt, String roles) {
|
||||||
|
this.id = id;
|
||||||
|
this.username = username;
|
||||||
|
this.online = online;
|
||||||
|
this.lastLoginAt = lastLoginAt;
|
||||||
|
this.lastLogoutAt = lastLogoutAt;
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public boolean isOnline() { return online; }
|
||||||
|
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
|
||||||
|
public LocalDateTime getLastLogoutAt() { return lastLogoutAt; }
|
||||||
|
public String getRoles() { return roles; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public String viewSessions(Model model) {
|
||||||
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
|
Set<String> activeUsernames = principals.stream()
|
||||||
|
.filter(principal -> principal instanceof UserDetails)
|
||||||
|
.map(principal -> ((UserDetails) principal).getUsername())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<AppUser> allUsers = userRepository.findAll();
|
||||||
|
|
||||||
|
List<UserSessionDTO> userSessions = allUsers.stream().map(user -> {
|
||||||
|
boolean isOnline = activeUsernames.contains(user.getUsername());
|
||||||
|
String rolesStr = user.getRoles().stream()
|
||||||
|
.map(Role::getName)
|
||||||
|
.map(r -> r.replace("ROLE_", ""))
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
return new UserSessionDTO(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
isOnline,
|
||||||
|
user.getLastLoginAt(),
|
||||||
|
user.getLastLogoutAt(),
|
||||||
|
rolesStr
|
||||||
|
);
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
long activeCount = userSessions.stream().filter(UserSessionDTO::isOnline).count();
|
||||||
|
long offlineCount = userSessions.size() - activeCount;
|
||||||
|
|
||||||
|
model.addAttribute("userSessions", userSessions);
|
||||||
|
model.addAttribute("activeCount", activeCount);
|
||||||
|
model.addAttribute("offlineCount", offlineCount);
|
||||||
|
model.addAttribute("totalCount", userSessions.size());
|
||||||
|
return "admin/sessions";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,20 +410,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-1.5 mb-1.5">
|
<div class="grid grid-cols-2 gap-1.5 mb-1.5">
|
||||||
<div>
|
<div th:if="${dot.equipement.taillesDisponibles != null and !dot.equipement.taillesDisponibles.trim().isEmpty()}">
|
||||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
|
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
|
||||||
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" placeholder="S, M, L..."
|
<select th:name="'taille_' + ${dot.id}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
||||||
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
<option value="">Choisir...</option>
|
||||||
|
<option th:if="${dot.taille != null and !dot.taille.isEmpty() and !#strings.contains(dot.equipement.taillesDisponibles, dot.taille)}"
|
||||||
|
th:value="${dot.taille}"
|
||||||
|
th:text="${dot.taille} + ' (Actuelle)'"
|
||||||
|
selected></option>
|
||||||
|
<option th:each="t : ${#strings.arraySplit(dot.equipement.taillesDisponibles, ',')}"
|
||||||
|
th:value="${#strings.trim(t)}"
|
||||||
|
th:text="${#strings.trim(t)}"
|
||||||
|
th:selected="${dot.isTailleOptionSelected(#strings.trim(t))}"></option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
@@ -408,6 +490,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 +542,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 +573,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 +613,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 +636,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 +655,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 +671,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 +686,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) {
|
||||||
|
if (catSexe === adherentSexe) {
|
||||||
catSelect.value = opt.value;
|
catSelect.value = opt.value;
|
||||||
preSelectedCatId = opt.value;
|
preSelectedCatId = opt.value;
|
||||||
foundCat = true;
|
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 +750,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 +820,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 +856,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 +1018,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=|{"page": ${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=|{"page": ${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=|{"page": ${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,123 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Utilisateurs & Sessions - 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">Suivi des Connectivité & Sessions</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main section -->
|
||||||
|
<div class="flex-1 overflow-auto p-6">
|
||||||
|
|
||||||
|
<!-- Cards summary -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||||
|
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-500">Total Utilisateurs</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900 mt-1" th:text="${totalCount}">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center font-bold">
|
||||||
|
👥
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-500">Sessions Actives (En ligne)</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600 mt-1" th:text="${activeCount}">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-green-50 text-green-600 flex items-center justify-center font-bold">
|
||||||
|
🟢
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-500">Hors Ligne</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-600 mt-1" th:text="${offlineCount}">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-gray-100 text-gray-500 flex items-center justify-center font-bold">
|
||||||
|
⚪
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header & Action -->
|
||||||
|
<div class="mb-6 flex justify-between items-center">
|
||||||
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
|
Liste des Utilisateurs
|
||||||
|
</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 flex items-center space-x-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Rafraîchir</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Table -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Utilisateur</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Rôle(s)</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Statut</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Dernière Connexion</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Dernière Déconnexion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
|
<tr th:if="${#lists.isEmpty(userSessions)}">
|
||||||
|
<td colspan="5" class="py-8 text-center text-gray-500">Aucun utilisateur enregistré.</td>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="userSession : ${userSessions}" 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 text-xs">
|
||||||
|
<span th:text="${#strings.substring(userSession.username, 0, 1).toUpperCase()}">U</span>
|
||||||
|
</div>
|
||||||
|
<span th:text="${userSession.username}">username</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<span class="px-2.5 py-0.5 bg-gray-100 text-gray-700 text-xs font-medium rounded border border-gray-200" th:text="${userSession.roles}">ADMIN</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<span th:if="${userSession.online}" class="inline-flex items-center px-2.5 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded-full">
|
||||||
|
<span class="w-2 h-2 mr-1.5 bg-green-500 rounded-full animate-pulse"></span>
|
||||||
|
En ligne
|
||||||
|
</span>
|
||||||
|
<span th:unless="${userSession.online}" class="inline-flex items-center px-2.5 py-1 bg-gray-100 text-gray-600 text-xs font-medium rounded-full">
|
||||||
|
<span class="w-2 h-2 mr-1.5 bg-gray-400 rounded-full"></span>
|
||||||
|
Hors ligne
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600 font-mono text-xs">
|
||||||
|
<span th:text="${userSession.lastLoginAt != null ? #temporals.format(userSession.lastLoginAt, 'dd/MM/yyyy HH:mm:ss') : 'Jamais'}">01/01/2026 10:00:00</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600 font-mono text-xs">
|
||||||
|
<span th:text="${userSession.lastLogoutAt != null ? #temporals.format(userSession.lastLogoutAt, 'dd/MM/yyyy HH:mm:ss') : '-'}">01/01/2026 12:00:00</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">
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
hx-target="#paiements-table-container"
|
hx-target="#paiements-table-container"
|
||||||
hx-select="#paiements-table-container"
|
hx-select="#paiements-table-container"
|
||||||
hx-include="#filter-form"
|
hx-include="#filter-form"
|
||||||
th:attr="hx-vals=|{'page': ${currentPage - 1}}|"
|
th:attr="hx-vals=|{"page": ${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>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
hx-target="#paiements-table-container"
|
hx-target="#paiements-table-container"
|
||||||
hx-select="#paiements-table-container"
|
hx-select="#paiements-table-container"
|
||||||
hx-include="#filter-form"
|
hx-include="#filter-form"
|
||||||
th:attr="hx-vals=|{'page': ${pageNum}}|"
|
th:attr="hx-vals=|{"page": ${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'}">
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
hx-target="#paiements-table-container"
|
hx-target="#paiements-table-container"
|
||||||
hx-select="#paiements-table-container"
|
hx-select="#paiements-table-container"
|
||||||
hx-include="#filter-form"
|
hx-include="#filter-form"
|
||||||
th:attr="hx-vals=|{'page': ${currentPage + 1}}|"
|
th:attr="hx-vals=|{"page": ${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>
|
||||||
|
|||||||
@@ -22,10 +22,23 @@
|
|||||||
<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 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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label>
|
<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">
|
<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>
|
||||||
|
<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 class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -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">
|
|
||||||
|
<div class="flex space-x-2 mb-4">
|
||||||
|
<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">
|
||||||
|
<option value="">Sélectionner un équipement à ajouter...</option>
|
||||||
<th:block th:each="equi : ${allEquipements}">
|
<th:block th:each="equi : ${allEquipements}">
|
||||||
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100">
|
<option th:value="${equi.id}" th:text="${equi.nom}"></option>
|
||||||
<div class="w-1/3">
|
</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,10 +20,13 @@
|
|||||||
<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>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<input type="text" id="searchInput" placeholder="Rechercher une catégorie..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
|
||||||
<a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
<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
|
+ Nouvelle Catégorie
|
||||||
</a>
|
</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">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -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,10 +20,13 @@
|
|||||||
<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>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<input type="text" id="searchInput" placeholder="Rechercher un équipement..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
|
||||||
<a th:href="@{/equipements/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
<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
|
+ Nouvel Équipement
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left border-collapse">
|
||||||
<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">
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
<div id="equipements-table-container" class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
|
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(dotations)}">
|
<tr th:if="${#lists.isEmpty(dotations)}">
|
||||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucun résultat trouvé pour cette recherche.</td>
|
<td colspan="7" class="py-8 text-center text-gray-500">Aucun résultat trouvé pour cette recherche.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="dotation : ${dotations}" class="hover:bg-gray-50">
|
<tr th:each="dotation : ${dotations}" class="hover:bg-gray-50">
|
||||||
<td class="py-3 px-4 font-medium text-gray-900">
|
<td class="py-3 px-4 font-medium text-gray-900">
|
||||||
@@ -92,6 +92,64 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Pagination Footer -->
|
||||||
|
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between bg-gray-50 text-sm text-gray-700">
|
||||||
|
<div th:if="${totalElements > 0}">
|
||||||
|
Affichage de <span class="font-semibold" th:text="${currentPage * pageSize + 1}">1</span> à
|
||||||
|
<span class="font-semibold" th:text="${T(java.lang.Math).min((currentPage + 1) * pageSize, totalElements)}">20</span> sur
|
||||||
|
<span class="font-semibold" th:text="${totalElements}">100</span> équipements
|
||||||
|
</div>
|
||||||
|
<div th:if="${totalElements == 0}">
|
||||||
|
Aucun équipement à afficher
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2" th:if="${totalPages > 1}">
|
||||||
|
<!-- Page Précédente -->
|
||||||
|
<a th:if="${currentPage > 0}"
|
||||||
|
th:href="@{/admin/equipements/recherche(equipementId=${equipementId},categorieId=${categorieId},fourni=${fourni},page=${currentPage - 1})}"
|
||||||
|
hx-get="/admin/equipements/recherche"
|
||||||
|
hx-target="#equipements-table-container"
|
||||||
|
hx-select="#equipements-table-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
th:attr="hx-vals=|{"equipementId": "${equipementId != null ? equipementId : ''}", "categorieId": "${categorieId != null ? categorieId : ''}", "fourni": "${fourni != null ? fourni : ''}", "page": ${currentPage - 1}}|"
|
||||||
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||||
|
Précédent
|
||||||
|
</a>
|
||||||
|
<span th:if="${currentPage == 0}" class="px-3 py-1 border border-gray-200 rounded text-gray-400 bg-gray-50 cursor-not-allowed">
|
||||||
|
Précédent
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Numéros de pages -->
|
||||||
|
<th:block th:each="pageNum : ${#numbers.sequence(0, totalPages - 1)}" th:if="${totalPages > 0}">
|
||||||
|
<a th:href="@{/admin/equipements/recherche(equipementId=${equipementId},categorieId=${categorieId},fourni=${fourni},page=${pageNum})}"
|
||||||
|
hx-get="/admin/equipements/recherche"
|
||||||
|
hx-target="#equipements-table-container"
|
||||||
|
hx-select="#equipements-table-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
th:attr="hx-vals=|{"equipementId": "${equipementId != null ? equipementId : ''}", "categorieId": "${categorieId != null ? categorieId : ''}", "fourni": "${fourni != null ? fourni : ''}", "page": ${pageNum}}|"
|
||||||
|
th:text="${pageNum + 1}"
|
||||||
|
class="px-3 py-1 rounded transition-colors"
|
||||||
|
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<!-- Page Suivante -->
|
||||||
|
<a th:if="${currentPage < totalPages - 1}"
|
||||||
|
th:href="@{/admin/equipements/recherche(equipementId=${equipementId},categorieId=${categorieId},fourni=${fourni},page=${currentPage + 1})}"
|
||||||
|
hx-get="/admin/equipements/recherche"
|
||||||
|
hx-target="#equipements-table-container"
|
||||||
|
hx-select="#equipements-table-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
th:attr="hx-vals=|{"equipementId": "${equipementId != null ? equipementId : ''}", "categorieId": "${categorieId != null ? categorieId : ''}", "fourni": "${fourni != null ? fourni : ''}", "page": ${currentPage + 1}}|"
|
||||||
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||||
|
Suivant
|
||||||
|
</a>
|
||||||
|
<span th:if="${currentPage == totalPages - 1}" class="px-3 py-1 border border-gray-200 rounded text-gray-400 bg-gray-50 cursor-not-allowed">
|
||||||
|
Suivant
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div id="licences-table-container" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-left border-collapse min-w-[900px]">
|
<table class="w-full text-left border-collapse min-w-[900px]">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -96,6 +96,64 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Footer -->
|
||||||
|
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between bg-gray-50 text-sm text-gray-700">
|
||||||
|
<div th:if="${totalElements > 0}">
|
||||||
|
Affichage de <span class="font-semibold" th:text="${currentPage * pageSize + 1}">1</span> à
|
||||||
|
<span class="font-semibold" th:text="${T(java.lang.Math).min((currentPage + 1) * pageSize, totalElements)}">20</span> sur
|
||||||
|
<span class="font-semibold" th:text="${totalElements}">100</span> licences
|
||||||
|
</div>
|
||||||
|
<div th:if="${totalElements == 0}">
|
||||||
|
Aucune licence à afficher
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2" th:if="${totalPages > 1}">
|
||||||
|
<!-- Page Précédente -->
|
||||||
|
<a th:if="${currentPage > 0}"
|
||||||
|
th:href="@{/admin/licences/recherche(nom=${nomFilter},prenom=${prenomFilter},categorieId=${categorieId},numeroLicence=${numeroLicenceFilter},email=${emailFilter},typeDemande=${typeDemandeFilter},page=${currentPage - 1})}"
|
||||||
|
hx-get="/admin/licences/recherche"
|
||||||
|
hx-target="#licences-table-container"
|
||||||
|
hx-select="#licences-table-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
th:attr="hx-vals=|{"nom": "${nomFilter != null ? nomFilter : ''}", "prenom": "${prenomFilter != null ? prenomFilter : ''}", "categorieId": "${categorieId != null ? categorieId : ''}", "numeroLicence": "${numeroLicenceFilter != null ? numeroLicenceFilter : ''}", "email": "${emailFilter != null ? emailFilter : ''}", "typeDemande": "${typeDemandeFilter != null ? typeDemandeFilter : ''}", "page": ${currentPage - 1}}|"
|
||||||
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||||
|
Précédent
|
||||||
|
</a>
|
||||||
|
<span th:if="${currentPage == 0}" class="px-3 py-1 border border-gray-200 rounded text-gray-400 bg-gray-50 cursor-not-allowed">
|
||||||
|
Précédent
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Numéros de pages -->
|
||||||
|
<th:block th:each="pageNum : ${#numbers.sequence(0, totalPages - 1)}" th:if="${totalPages > 0}">
|
||||||
|
<a th:href="@{/admin/licences/recherche(nom=${nomFilter},prenom=${prenomFilter},categorieId=${categorieId},numeroLicence=${numeroLicenceFilter},email=${emailFilter},typeDemande=${typeDemandeFilter},page=${pageNum})}"
|
||||||
|
hx-get="/admin/licences/recherche"
|
||||||
|
hx-target="#licences-table-container"
|
||||||
|
hx-select="#licences-table-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
th:attr="hx-vals=|{"nom": "${nomFilter != null ? nomFilter : ''}", "prenom": "${prenomFilter != null ? prenomFilter : ''}", "categorieId": "${categorieId != null ? categorieId : ''}", "numeroLicence": "${numeroLicenceFilter != null ? numeroLicenceFilter : ''}", "email": "${emailFilter != null ? emailFilter : ''}", "typeDemande": "${typeDemandeFilter != null ? typeDemandeFilter : ''}", "page": ${pageNum}}|"
|
||||||
|
th:text="${pageNum + 1}"
|
||||||
|
class="px-3 py-1 rounded transition-colors"
|
||||||
|
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<!-- Page Suivante -->
|
||||||
|
<a th:if="${currentPage < totalPages - 1}"
|
||||||
|
th:href="@{/admin/licences/recherche(nom=${nomFilter},prenom=${prenomFilter},categorieId=${categorieId},numeroLicence=${numeroLicenceFilter},email=${emailFilter},typeDemande=${typeDemandeFilter},page=${currentPage + 1})}"
|
||||||
|
hx-get="/admin/licences/recherche"
|
||||||
|
hx-target="#licences-table-container"
|
||||||
|
hx-select="#licences-table-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
th:attr="hx-vals=|{"nom": "${nomFilter != null ? nomFilter : ''}", "prenom": "${prenomFilter != null ? prenomFilter : ''}", "categorieId": "${categorieId != null ? categorieId : ''}", "numeroLicence": "${numeroLicenceFilter != null ? numeroLicenceFilter : ''}", "email": "${emailFilter != null ? emailFilter : ''}", "typeDemande": "${typeDemandeFilter != null ? typeDemandeFilter : ''}", "page": ${currentPage + 1}}|"
|
||||||
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||||
|
Suivant
|
||||||
|
</a>
|
||||||
|
<span th:if="${currentPage == totalPages - 1}" class="px-3 py-1 border border-gray-200 rounded text-gray-400 bg-gray-50 cursor-not-allowed">
|
||||||
|
Suivant
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -52,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">
|
||||||
@@ -66,6 +91,11 @@
|
|||||||
<li>Pour les adhérents nés à l'étranger : Passeport + justificatif de domicile (du représentant légal en cas de mineur)</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">
|
||||||
@@ -79,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>
|
||||||
@@ -113,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">
|
||||||
@@ -184,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>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<groupId>com.astalange</groupId>
|
<groupId>com.astalange</groupId>
|
||||||
<artifactId>as-talange-parent</artifactId>
|
<artifactId>as-talange-parent</artifactId>
|
||||||
<version>1.4-SNAPSHOT</version>
|
<version>1.6</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<name>as-talange-parent</name>
|
<name>as-talange-parent</name>
|
||||||
|
|||||||
Reference in New Issue
Block a user