Compare commits
36 Commits
5e75767f92
...
prod/v1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ec330e6a8 | |||
| 66cd27995d | |||
| 4870d61f86 | |||
| 6ca3d0b765 | |||
| ba268a33bc | |||
| f641ccc519 | |||
| 6ea00db0dc | |||
| 64ad4be7dd | |||
| d30e795325 | |||
| e0388c7884 | |||
| 7cf29f6106 | |||
| bef01e3a0e | |||
| 9cf1d26018 | |||
| 94d672c0f6 | |||
| 8156c8f3d0 | |||
| 7249b45533 | |||
| 7e036c1966 | |||
| 194986f880 | |||
| d834b8d0d3 | |||
| e3abe53eb0 | |||
| 348d6e6ccf | |||
| e670e38343 | |||
| fe3a1ec36c | |||
| 44ae524626 | |||
| 829a69f1a5 | |||
| f8336fb407 | |||
| ab1094d29a | |||
| 3477461fb2 | |||
| bb48f919b0 | |||
| 5f2bf61829 | |||
| 2df3e792f1 | |||
| 8ecdc54028 | |||
| e8baeab0e0 | |||
| d0fde2f2e8 | |||
| 754a0c8dcc | |||
| bbbb3978b9 |
@@ -20,7 +20,7 @@ jobs:
|
||||
SERVER_URL="${{ github.server_url }}"
|
||||
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
||||
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
||||
git clone --depth 1 "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Run Maven Tests
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
SERVER_URL="${{ github.server_url }}"
|
||||
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
||||
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
||||
git clone --depth 1 "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Deploy with Docker Compose
|
||||
@@ -58,3 +58,20 @@ jobs:
|
||||
echo "CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY:-1x00000000000000000000AA}" >> .env
|
||||
docker compose down app || true
|
||||
docker compose up -d --build app
|
||||
|
||||
- name: Verify application startup
|
||||
run: |
|
||||
echo "Waiting for application to start..."
|
||||
COUNT=0
|
||||
while [ $COUNT -lt 30 ]; do
|
||||
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
|
||||
echo "Application is up and login page is accessible!"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting... ($COUNT/30)"
|
||||
sleep 5
|
||||
COUNT=$((COUNT+1))
|
||||
done
|
||||
echo "Application failed to start or login page is not accessible."
|
||||
docker logs astalange_app
|
||||
exit 1
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
SERVER_URL="${{ github.server_url }}"
|
||||
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
||||
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
||||
git clone --depth 1 "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Run Maven Tests
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
SERVER_URL="${{ github.server_url }}"
|
||||
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
||||
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
||||
git clone --depth 1 "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Deploy with Docker Compose
|
||||
@@ -50,7 +50,24 @@ jobs:
|
||||
run: |
|
||||
echo "POSTGRES_USER=${DB_USER:-myuser}" > .env
|
||||
echo "POSTGRES_PASSWORD=${DB_PASSWORD:-mypassword}" >> .env
|
||||
echo "CAPTCHA_SECRET=1x0000000000000000000000000000000AA" >> .env
|
||||
echo "CAPTCHA_SITEKEY=1x00000000000000000000AA" >> .env
|
||||
docker compose down app || true
|
||||
docker compose up -d --build app
|
||||
|
||||
|
||||
- name: Verify application startup
|
||||
run: |
|
||||
echo "Waiting for application to start..."
|
||||
COUNT=0
|
||||
while [ $COUNT -lt 30 ]; do
|
||||
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
|
||||
echo "Application is up and login page is accessible!"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting... ($COUNT/30)"
|
||||
sleep 5
|
||||
COUNT=$((COUNT+1))
|
||||
done
|
||||
echo "Application failed to start or login page is not accessible."
|
||||
docker logs astalange_app
|
||||
exit 1
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.4</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.4</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -35,9 +35,21 @@ public class Adherent {
|
||||
@Column(nullable = false, length = 100)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String telephone;
|
||||
|
||||
@Column(name = "representant_legal", length = 150)
|
||||
private String representantLegal;
|
||||
|
||||
@Column(name = "type_demande", length = 50)
|
||||
private String typeDemande;
|
||||
|
||||
@Column(name = "ancien_club", length = 150)
|
||||
private String ancienClub;
|
||||
|
||||
@Column(name = "ancienne_categorie", length = 50)
|
||||
private String ancienneCategorie;
|
||||
|
||||
private boolean residentTalange = true;
|
||||
|
||||
@Column(nullable = false, length = 10)
|
||||
@@ -78,9 +90,21 @@ public class Adherent {
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getTelephone() { return telephone; }
|
||||
public void setTelephone(String telephone) { this.telephone = telephone; }
|
||||
|
||||
public String getRepresentantLegal() { return representantLegal; }
|
||||
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
|
||||
|
||||
public String getTypeDemande() { return typeDemande; }
|
||||
public void setTypeDemande(String typeDemande) { this.typeDemande = typeDemande; }
|
||||
|
||||
public String getAncienClub() { return ancienClub; }
|
||||
public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; }
|
||||
|
||||
public String getAncienneCategorie() { return ancienneCategorie; }
|
||||
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
|
||||
|
||||
public boolean isResidentTalange() { return residentTalange; }
|
||||
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ public class Categorie {
|
||||
@Column(nullable = false, length = 20)
|
||||
private String nom;
|
||||
|
||||
@Column(nullable = false, length = 10)
|
||||
private String sexe = "MASCULIN";
|
||||
|
||||
@Column(name = "annee_min")
|
||||
private Integer anneeMin;
|
||||
|
||||
@@ -45,6 +48,9 @@ public class Categorie {
|
||||
public String getNom() { return 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 void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; }
|
||||
|
||||
@@ -84,4 +90,33 @@ public class Categorie {
|
||||
.map(CategorieEquipement::getPrix)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Transient
|
||||
public BigDecimal getEquipementPrixByNom(String nom) {
|
||||
if (categorieEquipements == null || nom == null) return BigDecimal.ZERO;
|
||||
return categorieEquipements.stream()
|
||||
.filter(ce -> ce.getEquipement() != null && nom.equalsIgnoreCase(ce.getEquipement().getNom()))
|
||||
.findFirst()
|
||||
.map(CategorieEquipement::getPrix)
|
||||
.map(p -> p == null ? BigDecimal.ZERO : p)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Transient
|
||||
public boolean isU11OrBelow() {
|
||||
if (nom == null) return false;
|
||||
String nomCat = nom.trim().toUpperCase();
|
||||
if (nomCat.startsWith("U")) {
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^U(\\d+)").matcher(nomCat);
|
||||
if (m.find()) {
|
||||
try {
|
||||
int age = Integer.parseInt(m.group(1));
|
||||
return age <= 11;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,19 @@ public class Dotation {
|
||||
@JoinColumn(name = "equipement_id", nullable = false)
|
||||
private Equipement equipement;
|
||||
|
||||
@Column(length = 10)
|
||||
@Column(length = 50)
|
||||
private String taille;
|
||||
|
||||
@Column(length = 50)
|
||||
private String couleur;
|
||||
|
||||
@Column(length = 50)
|
||||
private String flocage;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean fourni = false;
|
||||
|
||||
@Column(length = 10)
|
||||
@Column(length = 50)
|
||||
private String numero;
|
||||
|
||||
@Column(nullable = false)
|
||||
@@ -47,6 +50,9 @@ public class Dotation {
|
||||
public String getTaille() { return 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 void setFlocage(String flocage) { this.flocage = flocage; }
|
||||
|
||||
|
||||
@@ -14,9 +14,24 @@ public class Equipement {
|
||||
@Column(nullable = false, length = 100)
|
||||
private String nom;
|
||||
|
||||
@Column(length = 100)
|
||||
private String reference;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "gestion_sexe", nullable = false)
|
||||
private boolean gestionSexe = false;
|
||||
|
||||
@Column(name = "type_public", nullable = false, length = 20)
|
||||
private String typePublic = "TOUS"; // JOUEUR, GARDIEN, TOUS
|
||||
|
||||
@Column(name = "tailles_disponibles", length = 500)
|
||||
private String taillesDisponibles;
|
||||
|
||||
@Column(name = "couleurs_disponibles", length = 500)
|
||||
private String couleursDisponibles;
|
||||
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "saison_id", nullable = false)
|
||||
@@ -30,9 +45,24 @@ public class Equipement {
|
||||
public String getNom() { return nom; }
|
||||
public void setNom(String nom) { this.nom = nom; }
|
||||
|
||||
public String getReference() { return reference; }
|
||||
public void setReference(String reference) { this.reference = reference; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public boolean isGestionSexe() { return gestionSexe; }
|
||||
public void setGestionSexe(boolean gestionSexe) { this.gestionSexe = gestionSexe; }
|
||||
|
||||
public String getTypePublic() { return typePublic; }
|
||||
public void setTypePublic(String typePublic) { this.typePublic = typePublic; }
|
||||
|
||||
public String getTaillesDisponibles() { return taillesDisponibles; }
|
||||
public void setTaillesDisponibles(String taillesDisponibles) { this.taillesDisponibles = taillesDisponibles; }
|
||||
|
||||
public String getCouleursDisponibles() { return couleursDisponibles; }
|
||||
public void setCouleursDisponibles(String couleursDisponibles) { this.couleursDisponibles = couleursDisponibles; }
|
||||
|
||||
public Saison getSaison() { return saison; }
|
||||
public void setSaison(Saison saison) { this.saison = saison; }
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ public class Paiement {
|
||||
@Column(name = "gestionnaire")
|
||||
private String gestionnaire;
|
||||
|
||||
@Column(name = "numero_cheque", length = 50)
|
||||
private String numeroCheque;
|
||||
|
||||
@Column(name = "commentaire", columnDefinition = "TEXT")
|
||||
private String commentaire;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -48,4 +54,10 @@ public class Paiement {
|
||||
|
||||
public String getGestionnaire() { return 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)
|
||||
private String prenom;
|
||||
|
||||
@Column(nullable = false, length = 10)
|
||||
private String sexe;
|
||||
|
||||
@Column(name = "date_naissance", nullable = false)
|
||||
private LocalDate dateNaissance;
|
||||
|
||||
@@ -33,6 +36,9 @@ public class PreInscription {
|
||||
@Column(nullable = false, length = 150)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String telephone;
|
||||
|
||||
@Column(name = "representant_legal", length = 150)
|
||||
private String representantLegal;
|
||||
|
||||
@@ -49,6 +55,15 @@ public class PreInscription {
|
||||
@Column(name = "consentement_rgpd", nullable = false)
|
||||
private Boolean consentementRgpd = false;
|
||||
|
||||
@Column(name = "type_demande", nullable = false, length = 50)
|
||||
private String typeDemande = "NOUVELLE";
|
||||
|
||||
@Column(name = "ancien_club", length = 150)
|
||||
private String ancienClub;
|
||||
|
||||
@Column(name = "ancienne_categorie", length = 50)
|
||||
private String ancienneCategorie = "NA";
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
@@ -59,6 +74,9 @@ public class PreInscription {
|
||||
public String getPrenom() { return prenom; }
|
||||
public void setPrenom(String prenom) { this.prenom = prenom; }
|
||||
|
||||
public String getSexe() { return sexe; }
|
||||
public void setSexe(String sexe) { this.sexe = sexe; }
|
||||
|
||||
public LocalDate getDateNaissance() { return dateNaissance; }
|
||||
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
|
||||
|
||||
@@ -74,6 +92,9 @@ public class PreInscription {
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getTelephone() { return telephone; }
|
||||
public void setTelephone(String telephone) { this.telephone = telephone; }
|
||||
|
||||
public String getRepresentantLegal() { return representantLegal; }
|
||||
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
|
||||
|
||||
@@ -88,4 +109,13 @@ public class PreInscription {
|
||||
|
||||
public Boolean getConsentementRgpd() { return consentementRgpd; }
|
||||
public void setConsentementRgpd(Boolean consentementRgpd) { this.consentementRgpd = consentementRgpd; }
|
||||
|
||||
public String getTypeDemande() { return typeDemande; }
|
||||
public void setTypeDemande(String typeDemande) { this.typeDemande = typeDemande; }
|
||||
|
||||
public String getAncienClub() { return ancienClub; }
|
||||
public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; }
|
||||
|
||||
public String getAncienneCategorie() { return ancienneCategorie; }
|
||||
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,13 @@ import org.springframework.stereotype.Repository;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
@Repository
|
||||
public interface CategorieRepository extends JpaRepository<Categorie, Long> {
|
||||
List<Categorie> findBySaison(Saison saison);
|
||||
|
||||
@Query("SELECT c FROM Categorie c WHERE c.saison = :saison AND :annee BETWEEN c.anneeMin AND c.anneeMax")
|
||||
List<Categorie> findBySaisonAndAnnee(@Param("saison") Saison saison, @Param("annee") Integer annee);
|
||||
}
|
||||
|
||||
@@ -86,12 +86,21 @@ public class CategorieService {
|
||||
List<Dotation> currentDotations = licence.getDotations();
|
||||
List<Dotation> toRemove = new ArrayList<>();
|
||||
|
||||
// Remove dotations for equipements no longer in the category
|
||||
// Remove dotations for equipements no longer in the category or not matching public
|
||||
for (Dotation d : currentDotations) {
|
||||
boolean found = false;
|
||||
for (CategorieEquipement ce : categorieEquipements) {
|
||||
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
|
||||
String typePublic = ce.getEquipement().getTypePublic();
|
||||
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
|
||||
|
||||
boolean matchPublic = "TOUS".equals(typePublic) ||
|
||||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
|
||||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
|
||||
|
||||
if (matchPublic) {
|
||||
found = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +112,17 @@ public class CategorieService {
|
||||
|
||||
// Add missing dotations
|
||||
for (CategorieEquipement ce : categorieEquipements) {
|
||||
String typePublic = ce.getEquipement().getTypePublic();
|
||||
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
|
||||
|
||||
boolean matchPublic = "TOUS".equals(typePublic) ||
|
||||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
|
||||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
|
||||
|
||||
if (!matchPublic) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean found = false;
|
||||
for (Dotation d : currentDotations) {
|
||||
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
|
||||
@@ -117,8 +137,20 @@ public class CategorieService {
|
||||
Dotation newDotation = new Dotation();
|
||||
newDotation.setLicence(licence);
|
||||
newDotation.setEquipement(ce.getEquipement());
|
||||
newDotation.setChoisi(ce.getObligatoire()); // By default checked if obligatoire
|
||||
boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom());
|
||||
boolean isNouvelle = false;
|
||||
if (licence.getTypeDemande() != null &&
|
||||
(licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) &&
|
||||
licence.getCategorie() != null) {
|
||||
isNouvelle = true;
|
||||
}
|
||||
newDotation.setChoisi(ce.getObligatoire() || (isMaillot && isNouvelle)); // Checked if obligatoire or if it's a new registration
|
||||
newDotation.setFourni(false);
|
||||
newDotation.setTaille("");
|
||||
newDotation.setNumero("");
|
||||
if (licence.getAdherent() != null) {
|
||||
newDotation.setFlocage(licence.getAdherent().getNom().toUpperCase() + " " + licence.getAdherent().getPrenom());
|
||||
}
|
||||
currentDotations.add(newDotation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class DotationService {
|
||||
sb.append('\ufeff');
|
||||
|
||||
// Header
|
||||
sb.append("Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Taille;Flocage;Numéro\n");
|
||||
sb.append("Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Référence;Taille;Flocage;Numéro\n");
|
||||
|
||||
for (Dotation d : dotations) {
|
||||
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : "";
|
||||
@@ -33,6 +33,7 @@ public class DotationService {
|
||||
String poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : "";
|
||||
String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : "";
|
||||
String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : "";
|
||||
String reference = d.getEquipement() != null && d.getEquipement().getReference() != null ? d.getEquipement().getReference() : "";
|
||||
String taille = d.getTaille() != null ? d.getTaille() : "";
|
||||
String flocage = d.getFlocage() != null ? d.getFlocage() : "";
|
||||
String numero = d.getNumero() != null ? d.getNumero() : "";
|
||||
@@ -43,6 +44,7 @@ public class DotationService {
|
||||
.append(escapeCsv(poste)).append(";")
|
||||
.append(escapeCsv(sexe)).append(";")
|
||||
.append(escapeCsv(equipement)).append(";")
|
||||
.append(escapeCsv(reference)).append(";")
|
||||
.append(escapeCsv(taille)).append(";")
|
||||
.append(escapeCsv(flocage)).append(";")
|
||||
.append(escapeCsv(numero)).append("\n");
|
||||
@@ -57,7 +59,7 @@ public class DotationService {
|
||||
sb.append('\ufeff');
|
||||
|
||||
// Header
|
||||
sb.append("Saison;Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Taille;Flocage;Numéro;Fourni\n");
|
||||
sb.append("Saison;Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Référence;Taille;Flocage;Numéro;Fourni\n");
|
||||
|
||||
for (Dotation d : dotations) {
|
||||
String saison = d.getLicence().getSaison() != null ? d.getLicence().getSaison().getNom() : "";
|
||||
@@ -67,6 +69,7 @@ public class DotationService {
|
||||
String poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : "";
|
||||
String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : "";
|
||||
String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : "";
|
||||
String reference = d.getEquipement() != null && d.getEquipement().getReference() != null ? d.getEquipement().getReference() : "";
|
||||
String taille = d.getTaille() != null ? d.getTaille() : "";
|
||||
String flocage = d.getFlocage() != null ? d.getFlocage() : "";
|
||||
String numero = d.getNumero() != null ? d.getNumero() : "";
|
||||
@@ -79,6 +82,7 @@ public class DotationService {
|
||||
.append(escapeCsv(poste)).append(";")
|
||||
.append(escapeCsv(sexe)).append(";")
|
||||
.append(escapeCsv(equipement)).append(";")
|
||||
.append(escapeCsv(reference)).append(";")
|
||||
.append(escapeCsv(taille)).append(";")
|
||||
.append(escapeCsv(flocage)).append(";")
|
||||
.append(escapeCsv(numero)).append(";")
|
||||
|
||||
@@ -42,9 +42,13 @@ public class PreInscriptionService {
|
||||
adherent.setLieuNaissancePays(pre.getLieuNaissancePays());
|
||||
adherent.setNationalite(pre.getNationalite());
|
||||
adherent.setEmail(pre.getEmail());
|
||||
adherent.setTelephone(pre.getTelephone());
|
||||
adherent.setRepresentantLegal(pre.getRepresentantLegal());
|
||||
adherent.setTypeDemande(pre.getTypeDemande());
|
||||
adherent.setAncienClub(pre.getAncienClub());
|
||||
adherent.setAncienneCategorie(pre.getAncienneCategorie());
|
||||
adherent.setSexe(pre.getSexe());
|
||||
// Par défaut
|
||||
adherent.setSexe("MASCULIN");
|
||||
adherent.setTypeMaillot("JOUEUR");
|
||||
adherent.setResidentTalange(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE adherent ADD COLUMN telephone VARCHAR(20);
|
||||
ALTER TABLE pre_inscription ADD COLUMN telephone VARCHAR(20);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE equipement ADD COLUMN reference VARCHAR(100);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE pre_inscription
|
||||
ADD COLUMN type_demande VARCHAR(50) DEFAULT 'NOUVELLE' NOT NULL,
|
||||
ADD COLUMN ancien_club VARCHAR(150);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE adherent
|
||||
ADD COLUMN type_demande VARCHAR(50),
|
||||
ADD COLUMN ancien_club VARCHAR(150);
|
||||
+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);
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.4</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -41,6 +44,10 @@ public class SecurityConfig {
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessUrl("/login?logout")
|
||||
.permitAll()
|
||||
)
|
||||
.sessionManagement(session -> session
|
||||
.maximumSessions(100)
|
||||
.sessionRegistry(sessionRegistry())
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
@@ -57,4 +64,14 @@ public class SecurityConfig {
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionRegistry sessionRegistry() {
|
||||
return new SessionRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
||||
return new HttpSessionEventPublisher();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,11 +177,13 @@ public class AdherentController {
|
||||
Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null);
|
||||
if (existing != null) {
|
||||
boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance());
|
||||
boolean typeMaillotChanged = !existing.getTypeMaillot().equals(adherent.getTypeMaillot());
|
||||
|
||||
if (dateChanged) {
|
||||
if (dateChanged || typeMaillotChanged) {
|
||||
int newBirthYear = adherent.getDateNaissance().getYear();
|
||||
List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId());
|
||||
for (Licence licence : licences) {
|
||||
if (dateChanged) {
|
||||
Saison saison = licence.getSaison();
|
||||
Categorie newCat = categorieRepository.findBySaison(saison).stream()
|
||||
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
|
||||
@@ -190,9 +192,15 @@ public class AdherentController {
|
||||
if (newCat != null) {
|
||||
licence.setCategorie(newCat);
|
||||
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());
|
||||
|
||||
@@ -168,12 +168,14 @@ public class DotationController {
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
|
||||
|
||||
String taille = request.getParameter("taille_" + id);
|
||||
String couleur = request.getParameter("couleur_" + id);
|
||||
String numero = request.getParameter("numero_" + id);
|
||||
String flocage = request.getParameter("flocage_" + id);
|
||||
String choisiParam = request.getParameter("choisi_" + id);
|
||||
String fourniParam = request.getParameter("fourni_" + id);
|
||||
|
||||
if (taille != null) dotation.setTaille(taille);
|
||||
if (couleur != null) dotation.setCouleur(couleur);
|
||||
if (numero != null) dotation.setNumero(numero);
|
||||
if (flocage != null) dotation.setFlocage(flocage);
|
||||
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalControllerAdvice {
|
||||
|
||||
@Value("${app.version}")
|
||||
private String appVersion;
|
||||
|
||||
@ModelAttribute("requestURI")
|
||||
public String requestURI(final HttpServletRequest request) {
|
||||
return request.getRequestURI();
|
||||
}
|
||||
|
||||
@ModelAttribute("appVersion")
|
||||
public String appVersion() {
|
||||
return appVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,21 +82,8 @@ public class LicenceController {
|
||||
|
||||
licence = licenceRepository.save(licence);
|
||||
|
||||
// Auto-initialize dotations for all configured equipments of the category
|
||||
List<CategorieEquipement> catEquipements = categorie.getCategorieEquipements();
|
||||
if (catEquipements != null) {
|
||||
for (CategorieEquipement ce : catEquipements) {
|
||||
Dotation dotation = new Dotation();
|
||||
dotation.setLicence(licence);
|
||||
dotation.setEquipement(ce.getEquipement());
|
||||
dotation.setTaille("");
|
||||
dotation.setFlocage(adherent.getNom().toUpperCase() + " " + adherent.getPrenom());
|
||||
dotation.setNumero("");
|
||||
dotation.setFourni(false);
|
||||
dotation.setChoisi(Boolean.TRUE.equals(ce.getObligatoire()));
|
||||
dotationRepository.save(dotation);
|
||||
}
|
||||
}
|
||||
// Auto-initialize dotations via CategorieService to ensure consistency
|
||||
categorieService.syncDotationsForLicence(licence);
|
||||
|
||||
return "redirect:/adherents/" + id + "/edit";
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ public class PaiementController {
|
||||
@RequestParam BigDecimal montant,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
|
||||
@RequestParam Long modePaiementId,
|
||||
@RequestParam(required = false) String numeroCheque,
|
||||
@RequestParam(required = false) String commentaire,
|
||||
Principal principal) {
|
||||
|
||||
Licence licence = licenceRepository.findById(id)
|
||||
@@ -58,6 +60,8 @@ public class PaiementController {
|
||||
paiement.setModePaiement(mode);
|
||||
paiement.setMontant(montant);
|
||||
paiement.setDatePaiement(datePaiement);
|
||||
paiement.setNumeroCheque(numeroCheque);
|
||||
paiement.setCommentaire(commentaire);
|
||||
if (principal != null) {
|
||||
paiement.setGestionnaire(principal.getName());
|
||||
}
|
||||
@@ -82,6 +86,8 @@ public class PaiementController {
|
||||
@RequestParam BigDecimal montant,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
|
||||
@RequestParam Long modePaiementId,
|
||||
@RequestParam(required = false) String numeroCheque,
|
||||
@RequestParam(required = false) String commentaire,
|
||||
@RequestParam(required = false) String redirect,
|
||||
Principal principal) {
|
||||
|
||||
@@ -106,6 +112,8 @@ public class PaiementController {
|
||||
paiement.setModePaiement(mode);
|
||||
paiement.setMontant(montant);
|
||||
paiement.setDatePaiement(datePaiement);
|
||||
paiement.setNumeroCheque(numeroCheque);
|
||||
paiement.setCommentaire(commentaire);
|
||||
if (principal != null) {
|
||||
paiement.setGestionnaire(principal.getName());
|
||||
}
|
||||
|
||||
+71
-2
@@ -6,14 +6,20 @@ import com.astalange.core.entity.Saison;
|
||||
import com.astalange.core.repository.PreInscriptionRepository;
|
||||
import com.astalange.core.repository.TokenPreInscriptionRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.service.CaptchaValidationService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/inscription-public")
|
||||
@@ -23,15 +29,18 @@ public class PublicInscriptionController {
|
||||
private final TokenPreInscriptionRepository tokenRepository;
|
||||
private final PreInscriptionRepository preInscriptionRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
|
||||
public PublicInscriptionController(CaptchaValidationService captchaValidationService,
|
||||
TokenPreInscriptionRepository tokenRepository,
|
||||
PreInscriptionRepository preInscriptionRepository,
|
||||
SaisonRepository saisonRepository) {
|
||||
SaisonRepository saisonRepository,
|
||||
CategorieRepository categorieRepository) {
|
||||
this.captchaValidationService = captchaValidationService;
|
||||
this.tokenRepository = tokenRepository;
|
||||
this.preInscriptionRepository = preInscriptionRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
}
|
||||
|
||||
@Value("${captcha.sitekey:1x00000000000000000000AA}")
|
||||
@@ -65,7 +74,7 @@ public class PublicInscriptionController {
|
||||
|
||||
TokenPreInscription token = new TokenPreInscription();
|
||||
token.setValeurUuid(UUID.randomUUID().toString());
|
||||
token.setDateExpiration(LocalDateTime.now().plusMinutes(5));
|
||||
token.setDateExpiration(LocalDateTime.now().plusMinutes(15));
|
||||
tokenRepository.save(token);
|
||||
|
||||
return "redirect:/inscription-public/formulaire?token=" + token.getValeurUuid();
|
||||
@@ -116,9 +125,69 @@ public class PublicInscriptionController {
|
||||
token.setEstUtilise(true);
|
||||
tokenRepository.save(token);
|
||||
|
||||
if ("NOUVELLE".equals(preInscription.getTypeDemande())) {
|
||||
String tarifStr = "210 €"; // Default
|
||||
if (preInscription.getDateNaissance() != null) {
|
||||
int annee = preInscription.getDateNaissance().getYear();
|
||||
List<Categorie> categories = categorieRepository.findBySaisonAndAnnee(activeSaison, annee);
|
||||
if (!categories.isEmpty()) {
|
||||
Categorie cat = categories.get(0);
|
||||
if (cat.getTarifBase() != null) {
|
||||
BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
|
||||
if (prixMaillot == null) {
|
||||
prixMaillot = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
|
||||
tarifStr = tarifTotal.intValue() + " €";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirectAttributes.addFlashAttribute("prenom", preInscription.getPrenom());
|
||||
redirectAttributes.addFlashAttribute("tarif", tarifStr);
|
||||
redirectAttributes.addFlashAttribute("documents", java.util.Arrays.asList(
|
||||
"Photo d’identité",
|
||||
"Carte d’identité ou passeport de l'adhérent",
|
||||
"Livret de famille (si pas de pièce d’identité)",
|
||||
"Adresse e-mail de l'adhérent (représentant légal en cas de mineur)",
|
||||
"Pour les adhérents nés à l'étranger : Passeport + justificatif de domicile (du représentant légal en cas de mineur)"
|
||||
));
|
||||
redirectAttributes.addFlashAttribute("isNouvelle", true);
|
||||
}
|
||||
|
||||
return "redirect:/inscription-public/succes";
|
||||
}
|
||||
|
||||
@GetMapping("/api/tarif")
|
||||
@ResponseBody
|
||||
public String getTarif(@RequestParam(value = "dateNaissance", required = false) String dateNaissanceStr) {
|
||||
if (dateNaissanceStr == null || dateNaissanceStr.isEmpty()) {
|
||||
return "210 €";
|
||||
}
|
||||
try {
|
||||
LocalDate dateNaissance = LocalDate.parse(dateNaissanceStr);
|
||||
int annee = dateNaissance.getYear();
|
||||
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||
if (activeSaison == null) return "210 €";
|
||||
|
||||
List<Categorie> categories = categorieRepository.findBySaisonAndAnnee(activeSaison, annee);
|
||||
if (categories.isEmpty()) return "210 €";
|
||||
|
||||
Categorie cat = categories.get(0);
|
||||
if (cat.getTarifBase() != null) {
|
||||
BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
|
||||
if (prixMaillot == null) {
|
||||
prixMaillot = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
|
||||
return tarifTotal.intValue() + " €";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Return default on error
|
||||
}
|
||||
return "210 €";
|
||||
}
|
||||
|
||||
@GetMapping("/succes")
|
||||
public String showSucces() {
|
||||
return "public/succes";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/sessions")
|
||||
public class SessionController {
|
||||
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
public SessionController(SessionRegistry sessionRegistry) {
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public String viewSessions(Model model) {
|
||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||
List<String> activeUsers = principals.stream()
|
||||
.filter(principal -> principal instanceof UserDetails)
|
||||
.map(principal -> ((UserDetails) principal).getUsername())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
model.addAttribute("activeUsers", activeUsers);
|
||||
model.addAttribute("activeCount", activeUsers.size());
|
||||
return "admin/sessions";
|
||||
}
|
||||
}
|
||||
@@ -20,3 +20,6 @@ spring:
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
app:
|
||||
version: @project.version@
|
||||
|
||||
@@ -117,6 +117,15 @@
|
||||
th:classappend="${#fields.hasErrors('email')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Téléphone -->
|
||||
<div>
|
||||
<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}" 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"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@@ -139,6 +148,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
<!-- Type de demande (Read-only) -->
|
||||
<div>
|
||||
<label for="typeDemande" class="block text-sm font-medium text-gray-700 mb-1">Type de demande (Pré-inscription)</label>
|
||||
<input type="text" id="typeDemande" th:field="*{typeDemande}" readonly
|
||||
class="w-full border border-gray-200 bg-gray-50 rounded-lg px-4 py-2 text-sm text-gray-500 outline-none cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<!-- Ancien club -->
|
||||
<div>
|
||||
<label for="ancienClub" class="block text-sm font-medium text-gray-700 mb-1">Ancien club</label>
|
||||
<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 focus:border-blue-500 outline-none transition-colors"
|
||||
th:classappend="${#fields.hasErrors('ancienClub')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('ancienClub')}" th:errors="*{ancienClub}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ancienneCategorieBlock" class="grid grid-cols-1 md:grid-cols-2 gap-6 hidden-block mt-6">
|
||||
<div>
|
||||
<label for="ancienneCategorie" class="block text-sm font-medium text-gray-700 mb-1">Ancienne catégorie <span class="text-red-500">*</span></label>
|
||||
<select id="ancienneCategorie" th:field="*{ancienneCategorie}"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
|
||||
<option value="NA">NA (Je ne sais plus)</option>
|
||||
<option value="U6">U6</option>
|
||||
<option value="U7">U7</option>
|
||||
<option value="U8">U8</option>
|
||||
<option value="U9">U9</option>
|
||||
<option value="U10">U10</option>
|
||||
<option value="U11">U11</option>
|
||||
<option value="U12">U12</option>
|
||||
<option value="U13">U13</option>
|
||||
<option value="U14">U14</option>
|
||||
<option value="U15">U15</option>
|
||||
<option value="U16">U16</option>
|
||||
<option value="U17">U17</option>
|
||||
<option value="Senior">Senior</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Removed Adresse -->
|
||||
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
@@ -228,6 +278,7 @@
|
||||
<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">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-right font-medium">Montant</th>
|
||||
<th class="py-2 px-3 text-right font-medium pr-4">Actions</th>
|
||||
@@ -239,6 +290,14 @@
|
||||
<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>
|
||||
</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-right font-semibold text-green-600" th:text="${paiement.montant + ' €'}">50.00 €</td>
|
||||
<td class="py-2 px-3 text-right pr-4">
|
||||
@@ -250,7 +309,9 @@
|
||||
th:data-mode-id="${paiement.modePaiement.id}"
|
||||
th:data-licence-id="${lic.id}"
|
||||
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"
|
||||
title="Modifier">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -297,24 +358,17 @@
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-start mb-1 gap-1">
|
||||
<span class="font-semibold text-xs text-gray-800 truncate" th:text="${dot.equipement.nom}">Maillot</span>
|
||||
<span class="font-semibold text-xs text-gray-800 truncate" th:title="${dot.equipement.nom}" th:text="${dot.equipement.nom}">Maillot</span>
|
||||
<span class="text-[9px] px-1.5 py-0.5 rounded-full font-bold flex-shrink-0"
|
||||
th:classappend="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'bg-red-50 text-red-700 border border-red-100' : 'bg-gray-50 text-gray-600 border border-gray-100'}"
|
||||
th:text="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
|
||||
</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 -->
|
||||
<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"
|
||||
th:data-equipement="${dot.equipement.nom}"
|
||||
th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'} + ' - ' + ${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'}">
|
||||
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
|
||||
</span>
|
||||
</div>
|
||||
@@ -331,17 +385,43 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" placeholder="S, M, L..."
|
||||
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
||||
<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">
|
||||
<option value="">Choisir...</option>
|
||||
<option th:each="t : ${#strings.arraySplit(dot.equipement.taillesDisponibles, ',')}"
|
||||
th:value="${#strings.trim(t)}"
|
||||
th:text="${#strings.trim(t)}"
|
||||
th:selected="${dot.taille == #strings.trim(t)}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div th:if="${(dot.equipement.taillesDisponibles == null or dot.equipement.taillesDisponibles.trim().isEmpty()) and (dot.taille != null and !dot.taille.isEmpty())}">
|
||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
|
||||
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 outline-none bg-gray-50 text-gray-500" readonly>
|
||||
</div>
|
||||
|
||||
<div th:if="${dot.equipement.couleursDisponibles != null and !dot.equipement.couleursDisponibles.trim().isEmpty()}">
|
||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Couleur</label>
|
||||
<select th:name="'couleur_' + ${dot.id}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
||||
<option value="">Choisir...</option>
|
||||
<option th:each="c : ${#strings.arraySplit(dot.equipement.couleursDisponibles, ',')}"
|
||||
th:value="${#strings.trim(c)}"
|
||||
th:text="${#strings.trim(c)}"
|
||||
th:selected="${dot.couleur == #strings.trim(c)}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div th:if="${(dot.equipement.couleursDisponibles == null or dot.equipement.couleursDisponibles.trim().isEmpty()) and (dot.couleur != null and !dot.couleur.isEmpty())}">
|
||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Couleur</label>
|
||||
<input type="text" th:name="'couleur_' + ${dot.id}" th:value="${dot.couleur}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 outline-none bg-gray-50 text-gray-500" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-1.5 mb-1.5">
|
||||
<div>
|
||||
<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"
|
||||
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>
|
||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label>
|
||||
<input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur"
|
||||
@@ -381,6 +461,7 @@
|
||||
th:data-nom="${cat.nom}"
|
||||
th:data-annee-min="${cat.anneeMin}"
|
||||
th:data-annee-max="${cat.anneeMax}"
|
||||
th:data-sexe="${cat.sexe}"
|
||||
th:data-tarif-base="${cat.tarifBase}"
|
||||
th:data-tarif-exterieur="${cat.tarifExterieur}"
|
||||
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
|
||||
@@ -459,11 +540,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<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 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>
|
||||
</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">
|
||||
<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>
|
||||
@@ -491,11 +580,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<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 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>
|
||||
</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">
|
||||
<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>
|
||||
@@ -540,28 +637,55 @@
|
||||
form.action = '/adherents/' + adherentId + '/licences';
|
||||
document.getElementById('numeroLicenceInput').value = '';
|
||||
document.getElementById('etatSelect').value = 'Brouillon';
|
||||
document.getElementById('typeDemandeSelect').value = '';
|
||||
|
||||
// Pré-remplissage du type de demande
|
||||
const adherentTypeDemande = document.getElementById('typeDemande') ? document.getElementById('typeDemande').value : '';
|
||||
const typeDemandeSelect = document.getElementById('typeDemandeSelect');
|
||||
if (adherentTypeDemande === 'NOUVELLE') {
|
||||
typeDemandeSelect.value = 'Nouvelle demande';
|
||||
} else if (adherentTypeDemande === 'RENOUVELLEMENT') {
|
||||
typeDemandeSelect.value = 'Renouvellement';
|
||||
} else {
|
||||
typeDemandeSelect.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 adherentSexe = document.getElementById('sexe') ? document.getElementById('sexe').value : 'MASCULIN';
|
||||
let foundCat = false;
|
||||
let fallbackCatId = '';
|
||||
let preSelectedCatId = '';
|
||||
|
||||
if (dateNaissanceStr) {
|
||||
const birthYear = new Date(dateNaissanceStr).getFullYear();
|
||||
|
||||
Array.from(catSelect.options).forEach(opt => {
|
||||
if (!opt.value) return;
|
||||
const min = parseInt(opt.getAttribute('data-annee-min'));
|
||||
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 (catSexe === adherentSexe) {
|
||||
catSelect.value = opt.value;
|
||||
preSelectedCatId = opt.value;
|
||||
foundCat = true;
|
||||
}
|
||||
if (catSexe === 'MASCULIN') {
|
||||
fallbackCatId = opt.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!foundCat && fallbackCatId) {
|
||||
catSelect.value = fallbackCatId;
|
||||
preSelectedCatId = fallbackCatId;
|
||||
foundCat = true;
|
||||
}
|
||||
|
||||
if (!foundCat) {
|
||||
catSelect.value = '';
|
||||
}
|
||||
@@ -591,17 +715,35 @@
|
||||
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('editPaiementForm').action = '/paiements/' + paiementId + '/update';
|
||||
document.getElementById('editPaiementMontant').value = montant;
|
||||
document.getElementById('editPaiementMontant').max = maxAmount;
|
||||
document.getElementById('editPaiementDate').value = date;
|
||||
document.getElementById('editPaiementMode').value = modePaiementId;
|
||||
document.getElementById('editNumeroCheque').value = numeroCheque || '';
|
||||
document.getElementById('editCommentaire').value = commentaire || '';
|
||||
toggleChequeVisibility(document.getElementById('editPaiementMode'), 'editChequeBlock', 'editNumeroCheque');
|
||||
}
|
||||
function closeEditPaiementModal() {
|
||||
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() {
|
||||
const dateInput = document.getElementById('dateNaissance');
|
||||
const repBlock = document.getElementById('representantBlock');
|
||||
@@ -643,11 +785,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
const ancienClubInput = document.getElementById('ancienClub');
|
||||
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
|
||||
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
|
||||
|
||||
if (ancienClubInput) {
|
||||
ancienClubInput.addEventListener('input', function() {
|
||||
if (this.value.trim().length > 0) {
|
||||
ancienneCategorieBlock.classList.remove('hidden-block');
|
||||
ancienneCategorieSelect.required = true;
|
||||
} else {
|
||||
ancienneCategorieBlock.classList.add('hidden-block');
|
||||
ancienneCategorieSelect.required = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (ancienClubInput.value.trim().length > 0) {
|
||||
ancienneCategorieBlock.classList.remove('hidden-block');
|
||||
ancienneCategorieSelect.required = true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLicencesCategoryAndPrices() {
|
||||
if (!dateInput.value) return;
|
||||
const dob = new Date(dateInput.value);
|
||||
const birthYear = dob.getFullYear();
|
||||
const isResident = residentCheckbox ? residentCheckbox.checked : true;
|
||||
const adherentSexe = document.getElementById('sexe') ? document.getElementById('sexe').value : 'MASCULIN';
|
||||
|
||||
const categories = [];
|
||||
const catSelect = document.getElementById('categorieSelect');
|
||||
@@ -657,17 +821,21 @@
|
||||
const nom = opt.getAttribute('data-nom');
|
||||
const min = parseInt(opt.getAttribute('data-annee-min'));
|
||||
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 tarifExterieur = parseFloat(opt.getAttribute('data-tarif-exterieur') || '0');
|
||||
if (id) {
|
||||
categories.push({ id, nom, min, max, tarifBase, tarifExterieur });
|
||||
categories.push({ id, nom, min, max, sexe, tarifBase, tarifExterieur });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
document.querySelectorAll('.licence-row').forEach(row => {
|
||||
@@ -815,7 +983,10 @@
|
||||
}
|
||||
|
||||
if (sexeSelect) {
|
||||
sexeSelect.addEventListener('change', updateEquipmentStyleLabels);
|
||||
sexeSelect.addEventListener('change', function() {
|
||||
updateEquipmentStyleLabels();
|
||||
updateLicencesCategoryAndPrices();
|
||||
});
|
||||
}
|
||||
if (typeMaillotSelect) {
|
||||
typeMaillotSelect.addEventListener('change', updateEquipmentStyleLabels);
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
hx-target="#adherents-table-container"
|
||||
hx-select="#adherents-table-container"
|
||||
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||
th:attr="hx-vals=|{'page': ${currentPage - 1}}|"
|
||||
th:attr="hx-vals=|{"page": ${currentPage - 1}}|"
|
||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||
Précédent
|
||||
</button>
|
||||
@@ -190,7 +190,7 @@
|
||||
hx-target="#adherents-table-container"
|
||||
hx-select="#adherents-table-container"
|
||||
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||
th:attr="hx-vals=|{'page': ${pageNum}}|"
|
||||
th:attr="hx-vals=|{"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'}">
|
||||
@@ -205,7 +205,7 @@
|
||||
hx-target="#adherents-table-container"
|
||||
hx-select="#adherents-table-container"
|
||||
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||
th:attr="hx-vals=|{'page': ${currentPage + 1}}|"
|
||||
th:attr="hx-vals=|{"page": ${currentPage + 1}}|"
|
||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||
Suivant
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Utilisateurs Connectés - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
<!-- Header -->
|
||||
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Utilisateurs Connectés</h2>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
Sessions Actives (<span th:text="${activeCount}">0</span>)
|
||||
</h3>
|
||||
<button hx-get="/admin/sessions" hx-target="body" hx-swap="outerHTML" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<th class="py-3 px-6 font-medium text-left">Nom d'utilisateur</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Statut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(activeUsers)}">
|
||||
<td colspan="2" class="py-8 text-center text-gray-500">Aucun utilisateur connecté pour le moment.</td>
|
||||
</tr>
|
||||
<tr th:each="username : ${activeUsers}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900 flex items-center">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold mr-3">
|
||||
<span th:text="${#strings.substring(username, 0, 1).toUpperCase()}">U</span>
|
||||
</div>
|
||||
<span th:text="${username}">username</span>
|
||||
</td>
|
||||
<td class="py-4 px-6">
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded-full">En ligne</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,6 +44,7 @@
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div>
|
||||
<a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a>
|
||||
<a href="/admin/logs/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/admin/logs/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Logs Paiements</a>
|
||||
<a href="/admin/sessions" th:classappend="${requestURI != null and requestURI.startsWith('/admin/sessions') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs Connectés</a>
|
||||
</th:block>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
@@ -51,6 +52,7 @@
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
<div class="mt-2 text-[10px] text-gray-400">v<span th:text="${appVersion}">1.0-SNAPSHOT</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
<!-- 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">Tableau de bord</h2>
|
||||
<a href="/admin/equipements/export-commande" class="bg-indigo-50 text-indigo-600 border border-indigo-200 px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-100 transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
Export CSV Équipements
|
||||
</a>
|
||||
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
|
||||
@@ -22,10 +22,19 @@
|
||||
<form th:action="@{/categories}" th:object="${categorie}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label>
|
||||
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
|
||||
<select th:field="*{sexe}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option value="MASCULIN">Masculine</option>
|
||||
<option value="FEMININ">Féminine</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -53,30 +62,95 @@
|
||||
|
||||
<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>
|
||||
<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}">
|
||||
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100">
|
||||
<div class="w-1/3">
|
||||
<option th:value="${equi.id}" th:text="${equi.nom}"></option>
|
||||
</th:block>
|
||||
</select>
|
||||
<button type="button" onclick="addEquipement()" class="px-4 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">Ajouter</button>
|
||||
</div>
|
||||
|
||||
<div id="equipementsContainer" class="space-y-3 max-h-64 overflow-y-auto">
|
||||
<th:block th:each="equi : ${allEquipements}" th:if="${categorie.hasEquipement(equi.id)}">
|
||||
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row" th:id="'eq-row-' + ${equi.id}">
|
||||
<div class="w-1/4">
|
||||
<span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span>
|
||||
</div>
|
||||
<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">
|
||||
<option value="NON_LIE" th:selected="${!categorie.hasEquipement(equi.id)}">Non lié</option>
|
||||
<option value="OBLIGATOIRE" th:selected="${categorie.hasEquipement(equi.id) and categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option>
|
||||
<option value="OPTIONNEL" th:selected="${categorie.hasEquipement(equi.id) and !categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
|
||||
<option value="OBLIGATOIRE" th:selected="${categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option>
|
||||
<option value="OPTIONNEL" th:selected="${!categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
|
||||
</select>
|
||||
</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>
|
||||
<input type="number" step="0.01" th:name="'prixEquipement_' + ${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">
|
||||
</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>
|
||||
</th:block>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -20,10 +20,13 @@
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Liste des Catégories</h3>
|
||||
<div class="flex items-center space-x-4">
|
||||
<input type="text" id="searchInput" placeholder="Rechercher une catégorie..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
|
||||
<a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||
+ Nouvelle Catégorie
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
@@ -43,7 +46,10 @@
|
||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td>
|
||||
</tr>
|
||||
<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.anneeMax}">2011</td>
|
||||
<td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td>
|
||||
@@ -61,5 +67,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
document.getElementById('searchInput').addEventListener('keyup', function() {
|
||||
let filter = this.value.toLowerCase();
|
||||
let rows = document.querySelectorAll('tbody tr.hover\\:bg-gray-50');
|
||||
rows.forEach(row => {
|
||||
let text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,14 +27,46 @@
|
||||
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Référence</label>
|
||||
<input type="text" th:field="*{reference}" 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">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>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-100">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-4">Options de déclinaison (facultatif)</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" th:field="*{gestionSexe}" class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||
<label class="ml-2 block text-sm text-gray-700">Gérer les coupes Homme/Femme</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Type de public (Poste)</label>
|
||||
<select th:field="*{typePublic}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option value="TOUS">Tous (Joueurs et Gardiens)</option>
|
||||
<option value="JOUEUR">Joueurs de champ uniquement</option>
|
||||
<option value="GARDIEN">Gardiens de but uniquement</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Tailles disponibles</label>
|
||||
<input type="text" th:field="*{taillesDisponibles}" placeholder="ex: XS, S, M, L, XL ou 5/6 ANS, 7/8 ANS..." class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<p class="text-xs text-gray-500 mt-1">Séparez les tailles par des virgules. Laissez vide si taille unique ou standard.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Couleurs disponibles</label>
|
||||
<input type="text" th:field="*{couleursDisponibles}" placeholder="ex: Bleu, Rouge, Blanc..." class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<p class="text-xs text-gray-500 mt-1">Séparez les couleurs par des virgules. Laissez vide si couleur unique.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<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>
|
||||
|
||||
@@ -20,10 +20,13 @@
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Liste des Équipements</h3>
|
||||
<div class="flex items-center space-x-4">
|
||||
<input type="text" id="searchInput" placeholder="Rechercher un équipement..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
|
||||
<a th:href="@{/equipements/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||
+ Nouvel Équipement
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
@@ -51,5 +54,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
document.getElementById('searchInput').addEventListener('keyup', function() {
|
||||
let filter = this.value.toLowerCase();
|
||||
let rows = document.querySelectorAll('tbody tr.hover\\:bg-gray-50');
|
||||
rows.forEach(row => {
|
||||
let text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<th class="py-3 px-4 font-medium text-left">Poste / Sexe</th>
|
||||
<th class="py-3 px-4 font-medium text-left">Catégorie</th>
|
||||
<th class="py-3 px-4 font-medium text-left">Équipement</th>
|
||||
<th class="py-3 px-4 font-medium text-left">Référence</th>
|
||||
<th class="py-3 px-4 font-medium text-left">Taille / Floc.</th>
|
||||
<th class="py-3 px-4 font-medium text-center">Fourni</th>
|
||||
</tr>
|
||||
@@ -78,6 +79,7 @@
|
||||
</td>
|
||||
<td class="py-3 px-4 text-gray-600" th:text="${dotation.licence.categorie != null ? dotation.licence.categorie.nom : '-'}">Catégorie</td>
|
||||
<td class="py-3 px-4 text-gray-900 font-medium" th:text="${dotation.equipement != null ? dotation.equipement.nom : '-'}">Maillot</td>
|
||||
<td class="py-3 px-4 text-gray-600 font-mono text-xs" th:text="${dotation.equipement != null && dotation.equipement.reference != null ? dotation.equipement.reference : '-'}">REF-01</td>
|
||||
<td class="py-3 px-4 text-gray-600">
|
||||
<div th:text="'T: ' + (${dotation.taille != null && !dotation.taille.isEmpty() ? dotation.taille : '-'})">Taille</div>
|
||||
<div th:if="${dotation.flocage != null && !dotation.flocage.isEmpty()}" th:text="'F: ' + ${dotation.flocage}">Flocage</div>
|
||||
|
||||
@@ -6,7 +6,17 @@
|
||||
<title>AS Talange - Formulaire d'inscription</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style> body { font-family: 'Inter', sans-serif; } </style>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.was-validated input:invalid, .was-validated select:invalid {
|
||||
border-color: #ef4444 !important;
|
||||
background-color: #fef2f2 !important;
|
||||
}
|
||||
.was-validated input[type="radio"]:invalid, .was-validated input[type="checkbox"]:invalid {
|
||||
outline: 2px solid #ef4444;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 flex items-center justify-center min-h-screen py-12">
|
||||
<div class="max-w-xl w-full bg-white rounded-xl shadow-md p-8 border border-gray-100">
|
||||
@@ -14,9 +24,70 @@
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2 text-center">Formulaire de pré-inscription</h2>
|
||||
<p class="text-sm text-gray-500 mb-8 text-center">Veuillez remplir les informations concernant le futur licencié.</p>
|
||||
|
||||
<form th:action="@{/inscription-public/formulaire}" th:object="${preInscription}" method="post" class="space-y-6">
|
||||
<div class="mb-6 p-4 bg-blue-50 border-l-4 border-blue-500 text-blue-700 text-sm rounded">
|
||||
<h3 class="font-bold mb-1">Information sur le type de demande</h3>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li><strong>Nouvelle licence :</strong> Concerne les personnes qui n'étaient pas inscrites au club la saison précédente.</li>
|
||||
<li><strong>Renouvellement :</strong> Réservé aux personnes déjà inscrites au club la saison précédente.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/inscription-public/formulaire}" th:object="${preInscription}" method="post" class="space-y-6" novalidate>
|
||||
<input type="hidden" name="token" th:value="${tokenUuid}" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-gray-700">Type de demande <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="*{typeDemande}" value="NOUVELLE" 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">Nouvelle licence</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="radio" th:field="*{typeDemande}" value="RENOUVELLEMENT" 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">Renouvellement</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nouvelleLicenceBlock" class="hidden space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ancien club</label>
|
||||
<input type="text" id="ancienClub" th:field="*{ancienClub}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Nom du club (laisser vide si aucun)">
|
||||
</div>
|
||||
|
||||
<div id="ancienneCategorieBlock" class="hidden">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ancienne catégorie <span class="text-red-500">*</span></label>
|
||||
<select id="ancienneCategorie" th:field="*{ancienneCategorie}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option value="NA">NA (Je ne sais plus)</option>
|
||||
<option value="U6">U6</option>
|
||||
<option value="U7">U7</option>
|
||||
<option value="U8">U8</option>
|
||||
<option value="U9">U9</option>
|
||||
<option value="U10">U10</option>
|
||||
<option value="U11">U11</option>
|
||||
<option value="U12">U12</option>
|
||||
<option value="U13">U13</option>
|
||||
<option value="U14">U14</option>
|
||||
<option value="U15">U15</option>
|
||||
<option value="U16">U16</option>
|
||||
<option value="U17">U17</option>
|
||||
<option value="Senior">Senior</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-lg">
|
||||
<h4 class="font-bold text-yellow-800 mb-2">Documents et informations à fournir</h4>
|
||||
<p class="text-sm text-yellow-700 mb-2">Veuillez préparer les éléments suivants pour finaliser la licence :</p>
|
||||
<ul class="list-disc pl-5 text-sm text-yellow-700 space-y-1">
|
||||
<li>Photo d’identité</li>
|
||||
<li>Carte d’identité ou passeport de l'adhérent</li>
|
||||
<li>Livret de famille (si pas de pièce d’identité)</li>
|
||||
<li>Adresse e-mail de l'adhérent (représentant légal en cas de mineur)</li>
|
||||
<li>Pour les adhérents nés à l'étranger : Passeport + justificatif de domicile (du représentant légal en cas de mineur)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label>
|
||||
@@ -28,6 +99,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-gray-700">Sexe <span class="text-red-500">*</span></label>
|
||||
<div class="flex items-center space-x-6">
|
||||
<label class="flex items-center">
|
||||
<input type="radio" th:field="*{sexe}" value="MASCULIN" required class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">Masculin</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="radio" th:field="*{sexe}" value="FEMININ" required class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">Féminin</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
|
||||
@@ -56,10 +141,16 @@
|
||||
<input type="text" id="representantLegal" th:field="*{representantLegal}" placeholder="Nom et Prénom du parent" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Email <span class="text-red-500">*</span></label>
|
||||
<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>
|
||||
<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}" 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 class="flex items-start pt-2">
|
||||
<div class="flex items-center h-5">
|
||||
@@ -78,10 +169,76 @@
|
||||
<p class="text-[10px] text-gray-400 text-center mt-4 leading-tight">
|
||||
Les données sont stockées de manière sécurisée, accessibles uniquement au bureau du club. Vous pouvez demander leur suppression à tout moment.
|
||||
</p>
|
||||
<p class="text-[10px] text-gray-400 text-center mt-2">
|
||||
v<span th:text="${appVersion}">1.0-SNAPSHOT</span>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
|
||||
let errorMsg = document.getElementById('form-error-msg');
|
||||
if (!errorMsg) {
|
||||
errorMsg = document.createElement('div');
|
||||
errorMsg.id = 'form-error-msg';
|
||||
errorMsg.className = 'mb-6 p-4 bg-red-50 border-l-4 border-red-500 text-red-700 text-sm rounded transition-all duration-300';
|
||||
errorMsg.innerHTML = '<h3 class="font-bold">Erreur de validation</h3><p>Veuillez renseigner tous les champs obligatoires (encadrés en rouge).</p>';
|
||||
form.insertBefore(errorMsg, form.firstChild);
|
||||
}
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
|
||||
// Scroll to the first invalid element for better mobile UX
|
||||
const firstInvalid = form.querySelector(':invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
|
||||
const typeDemandeRadios = document.querySelectorAll('input[name="typeDemande"]');
|
||||
const nouvelleLicenceBlock = document.getElementById('nouvelleLicenceBlock');
|
||||
|
||||
typeDemandeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.value === 'NOUVELLE') {
|
||||
nouvelleLicenceBlock.classList.remove('hidden');
|
||||
} else {
|
||||
nouvelleLicenceBlock.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger on load if already checked (e.g. going back or validation error)
|
||||
const checkedRadio = document.querySelector('input[name="typeDemande"]:checked');
|
||||
if (checkedRadio && checkedRadio.value === 'NOUVELLE') {
|
||||
nouvelleLicenceBlock.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const ancienClubInput = document.getElementById('ancienClub');
|
||||
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
|
||||
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
|
||||
|
||||
if (ancienClubInput) {
|
||||
ancienClubInput.addEventListener('input', function() {
|
||||
if (this.value.trim().length > 0) {
|
||||
ancienneCategorieBlock.classList.remove('hidden');
|
||||
ancienneCategorieSelect.required = true;
|
||||
} else {
|
||||
ancienneCategorieBlock.classList.add('hidden');
|
||||
ancienneCategorieSelect.required = false;
|
||||
}
|
||||
});
|
||||
// Initial check
|
||||
if (ancienClubInput.value.trim().length > 0) {
|
||||
ancienneCategorieBlock.classList.remove('hidden');
|
||||
ancienneCategorieSelect.required = true;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('dateNaissance').addEventListener('change', function() {
|
||||
const dateInput = this.value;
|
||||
const repBlock = document.getElementById('representantBlock');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
||||
<meta charset="UTF-8">
|
||||
<title>Inscription réussie</title>
|
||||
@@ -16,8 +17,56 @@
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Pré-inscription envoyée !</h1>
|
||||
<p class="text-gray-600 mb-2">Votre dossier a bien été transmis au secrétariat de l'AS Talange.</p>
|
||||
<p class="text-gray-600 mb-2" th:text="'Votre dossier pour ' + ${prenom != null ? prenom : 'le futur licencié'} + ' a bien été transmis au secrétariat de l\'AS Talange.'">Votre dossier a bien été transmis au secrétariat de l'AS Talange.</p>
|
||||
<p class="text-sm text-gray-500">Un responsable va traiter votre demande prochainement. Vous pouvez fermer cette page.</p>
|
||||
|
||||
<div th:if="${isNouvelle}" class="mt-8 text-left">
|
||||
<div id="ticket-documents" class="bg-yellow-50 border-2 border-dashed border-yellow-300 p-6 rounded-lg relative overflow-hidden shadow-sm">
|
||||
<!-- Decorative elements for ticket look -->
|
||||
<div class="absolute -left-3 top-1/2 w-6 h-6 bg-white rounded-full border-r-2 border-yellow-300 transform -translate-y-1/2"></div>
|
||||
<div class="absolute -right-3 top-1/2 w-6 h-6 bg-white rounded-full border-l-2 border-yellow-300 transform -translate-y-1/2"></div>
|
||||
|
||||
<h2 class="text-lg font-bold text-yellow-800 mb-4 text-center border-b-2 border-yellow-200 pb-2 border-dashed uppercase tracking-wider">Mémo Inscription</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-sm font-semibold text-yellow-900">Tarif Licence : <span class="text-lg" th:text="${tarif}">210 €</span></p>
|
||||
</div>
|
||||
|
||||
<h3 class="font-semibold text-yellow-900 mb-2 text-sm uppercase tracking-wide">Documents et informations à fournir :</h3>
|
||||
<ul class="list-none space-y-2 mb-3">
|
||||
<li th:each="doc : ${documents}" class="flex items-start">
|
||||
<svg class="w-4 h-4 text-yellow-500 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>
|
||||
<span class="text-sm text-yellow-800" th:text="${doc}">Document</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-xs text-yellow-800 italic text-center mb-2">Ces documents sont à fournir au secrétariat du club pour finaliser l'inscription.</p>
|
||||
<div class="mt-4 pt-2 border-t-2 border-yellow-200 border-dashed text-center">
|
||||
<p class="text-xs text-yellow-600 font-medium">As Talange</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button type="button" onclick="captureTicket()" class="bg-gray-800 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg shadow transition-colors inline-flex items-center text-sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
Enregistrer cette liste (Image)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:if="${isNouvelle}">
|
||||
function captureTicket() {
|
||||
const ticket = document.getElementById('ticket-documents');
|
||||
html2canvas(ticket, { scale: 2 }).then(canvas => {
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.download = 'ASTalange-Documents.png';
|
||||
link.href = imgData;
|
||||
link.click();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user