Compare commits

25 Commits

Author SHA1 Message Date
ucef f93d6eafb3 chore: release version 1.3
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m46s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m44s
2026-06-25 11:05:01 +02:00
ucef e670e38343 feat: harmoniser les documents demandés sur la page de succès de pré-inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m26s
2026-06-25 10:34:41 +02:00
ucef fe3a1ec36c fix: forcer le retour visuel de validation sur mobile (novalidate)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m12s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m12s
2026-06-25 10:07:51 +02:00
ucef 44ae524626 ci: ajout d'une vérification de démarrage après le déploiement
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m47s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m22s
2026-06-25 09:47:54 +02:00
ucef 829a69f1a5 fix: corriger l'indentation YAML qui empêchait l'application de démarrer 2026-06-25 09:45:12 +02:00
ucef f8336fb407 feat: affichage du numéro de version de l'application
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 22s
2026-06-25 09:19:59 +02:00
ucef ab1094d29a feat: préciser le justificatif de domicile pour les adhérents nés à l'étranger 2026-06-25 09:12:21 +02:00
ucef 3477461fb2 feat: ajouter un retour visuel sur la validation des champs obligatoires (mobile)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m15s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m14s
2026-06-25 08:57:06 +02:00
ucef bb48f919b0 feat: rendre le message sur les documents à fournir plus générique
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m17s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m26s
2026-06-25 08:21:51 +02:00
ucef 5f2bf61829 feat: gestion type de demande, calcul dynamique du tarif et pre-selection equipement (maillot) pour inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m28s
2026-06-24 23:56:46 +02:00
ucef 2df3e792f1 feat(pre-inscription): augmentation de la durée de validité du token à 15 minutes
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m31s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m47s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m5s
2026-06-24 15:56:54 +02:00
ucef 8ecdc54028 fix(ci): injection des variables captcha dans la pipeline de test
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m20s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 40s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m59s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m4s
2026-06-24 10:48:55 +02:00
ucef e8baeab0e0 feat(equipement): ajout de la référence et retrait du bouton d'export du dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m54s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m13s
2026-06-24 10:18:53 +02:00
ucef d0fde2f2e8 feat(adherent): réintégration du numéro de téléphone 2026-06-24 10:18:44 +02:00
ucef 754a0c8dcc fix(security): ajout de l'import manquant pour @Value
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m38s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 1m54s
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m26s
2026-06-23 15:30:01 +02:00
ucef bbbb3978b9 fix(ci): retrait de clone --depth 1 pour permettre le checkout des branches secondaires
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 4m27s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Has been skipped
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Failing after 1m38s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Has been skipped
2026-06-23 15:14:53 +02:00
ucef 5e75767f92 fix(security): injection correcte de la clé de site captcha dans la vue
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Failing after 26s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Has been skipped
2026-06-23 02:05:27 +02:00
ucef 2f8c8f0829 fix(docker): ajout de la politique de redémarrage automatique pour la BDD et pgAdmin 2026-06-23 02:01:55 +02:00
ucef bf38e01c49 fix(docker): transmission des variables captcha au conteneur
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m2s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 31s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m33s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 11s
2026-06-23 01:47:50 +02:00
ucef 6ea793de87 ci(gitea): séparation des pipelines test et prod en deux fichiers distincts
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m4s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m59s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 2m28s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m41s
2026-06-23 01:16:59 +02:00
ucef 60257f4823 feat(ui): ajout du logo sur la page de connexion 2026-06-23 01:05:55 +02:00
ucef 5c64184c93 feat(ui): ajout d'une bannière de bienvenue avec logo sur le tableau de bord
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-23 01:03:03 +02:00
ucef 2dc39521c4 perf: redimensionnement et optimisation du logo pour un chargement rapide 2026-06-23 01:01:53 +02:00
ucef c7d8aaa179 style: mise à jour du logo du club
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m5s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-23 00:49:46 +02:00
ucef 62d1158a8a ci(gitea): utilisation du label ubuntu-latest pour l'environnement de test
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m46s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-23 00:40:55 +02:00
33 changed files with 523 additions and 71 deletions
@@ -1,15 +1,14 @@
name: AS Talange CI/CD Pipeline name: AS Talange CI/CD Pipeline - Production
on: on:
push: push:
branches: branches:
- main
- 'prod/*' - 'prod/*'
jobs: jobs:
test: test:
name: Build & Run Unit Tests name: Build & Run Unit Tests
runs-on: self-hosted runs-on: [self-hosted, prod]
container: container:
image: maven:3.9.6-eclipse-temurin-21 image: maven:3.9.6-eclipse-temurin-21
steps: steps:
@@ -21,38 +20,13 @@ jobs:
SERVER_URL="${{ github.server_url }}" SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}" SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}" 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 }} git checkout ${{ github.sha }}
- name: Run Maven Tests - name: Run Maven Tests
run: mvn clean test run: mvn clean test
deploy_test:
name: Deploy to Test Environment
needs: test
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, test]
container:
image: docker:latest
steps:
- name: Clone repository
run: |
apk add --no-cache git
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 checkout ${{ github.sha }}
- name: Deploy with Docker Compose
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
echo "POSTGRES_USER=${DB_USER:-myuser}" > .env
echo "POSTGRES_PASSWORD=${DB_PASSWORD:-mypassword}" >> .env
docker compose down app || true
docker compose up -d --build app
deploy_prod: deploy_prod:
name: Deploy to Prod Environment name: Deploy to Prod Environment
@@ -68,7 +42,7 @@ jobs:
SERVER_URL="${{ github.server_url }}" SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}" SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}" 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 }} git checkout ${{ github.sha }}
- name: Deploy with Docker Compose - name: Deploy with Docker Compose
@@ -84,3 +58,20 @@ jobs:
echo "CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY:-1x00000000000000000000AA}" >> .env echo "CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY:-1x00000000000000000000AA}" >> .env
docker compose down app || true docker compose down app || true
docker compose up -d --build app docker compose up -d --build app
- name: Verify application startup
run: |
echo "Waiting for application to start..."
COUNT=0
while [ $COUNT -lt 30 ]; do
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
echo "Application is up and login page is accessible!"
exit 0
fi
echo "Waiting... ($COUNT/30)"
sleep 5
COUNT=$((COUNT+1))
done
echo "Application failed to start or login page is not accessible."
docker logs astalange_app
exit 1
+73
View File
@@ -0,0 +1,73 @@
name: AS Talange CI/CD Pipeline
on:
push:
branches:
- main
jobs:
test:
name: Build & Run Unit Tests
runs-on: [self-hosted, ubuntu-latest]
container:
image: maven:3.9.6-eclipse-temurin-21
steps:
- name: Clone repository
run: |
if ! command -v git &> /dev/null; then
apt-get update && apt-get install -y git
fi
SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
git checkout ${{ github.sha }}
- name: Run Maven Tests
run: mvn clean test
deploy_test:
name: Deploy to Test Environment
needs: test
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, ubuntu-latest]
container:
image: docker:latest
steps:
- name: Clone repository
run: |
apk add --no-cache git
SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
git checkout ${{ github.sha }}
- name: Deploy with Docker Compose
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
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
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.3</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.3</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -35,9 +35,18 @@ public class Adherent {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String email; private String email;
@Column(length = 20)
private String telephone;
@Column(name = "representant_legal", length = 150) @Column(name = "representant_legal", length = 150)
private String representantLegal; private String representantLegal;
@Column(name = "type_demande", length = 50)
private String typeDemande;
@Column(name = "ancien_club", length = 150)
private String ancienClub;
private boolean residentTalange = true; private boolean residentTalange = true;
@Column(nullable = false, length = 10) @Column(nullable = false, length = 10)
@@ -78,9 +87,18 @@ public class Adherent {
public String getEmail() { return email; } public String getEmail() { return email; }
public void setEmail(String email) { this.email = 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 String getRepresentantLegal() { return representantLegal; }
public void setRepresentantLegal(String representantLegal) { this.representantLegal = 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 boolean isResidentTalange() { return residentTalange; } public boolean isResidentTalange() { return residentTalange; }
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; } public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
@@ -84,4 +84,33 @@ public class Categorie {
.map(CategorieEquipement::getPrix) .map(CategorieEquipement::getPrix)
.orElse(BigDecimal.ZERO); .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;
}
} }
@@ -14,6 +14,9 @@ public class Equipement {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String nom; private String nom;
@Column(length = 100)
private String reference;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String description; private String description;
@@ -30,6 +33,9 @@ public class Equipement {
public String getNom() { return nom; } public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; } public void setNom(String nom) { this.nom = nom; }
public String getReference() { return reference; }
public void setReference(String reference) { this.reference = reference; }
public String getDescription() { return description; } public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; } public void setDescription(String description) { this.description = description; }
@@ -33,6 +33,9 @@ public class PreInscription {
@Column(nullable = false, length = 150) @Column(nullable = false, length = 150)
private String email; private String email;
@Column(length = 20)
private String telephone;
@Column(name = "representant_legal", length = 150) @Column(name = "representant_legal", length = 150)
private String representantLegal; private String representantLegal;
@@ -49,6 +52,12 @@ public class PreInscription {
@Column(name = "consentement_rgpd", nullable = false) @Column(name = "consentement_rgpd", nullable = false)
private Boolean consentementRgpd = 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;
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
public void setId(Long id) { this.id = id; } public void setId(Long id) { this.id = id; }
@@ -74,6 +83,9 @@ public class PreInscription {
public String getEmail() { return email; } public String getEmail() { return email; }
public void setEmail(String email) { this.email = 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 String getRepresentantLegal() { return representantLegal; }
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; } public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
@@ -88,4 +100,10 @@ public class PreInscription {
public Boolean getConsentementRgpd() { return consentementRgpd; } public Boolean getConsentementRgpd() { return consentementRgpd; }
public void setConsentementRgpd(Boolean consentementRgpd) { this.consentementRgpd = 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; }
} }
@@ -7,7 +7,13 @@ import org.springframework.stereotype.Repository;
import com.astalange.core.entity.Saison; import com.astalange.core.entity.Saison;
import java.util.List; import java.util.List;
import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.Query;
@Repository @Repository
public interface CategorieRepository extends JpaRepository<Categorie, Long> { public interface CategorieRepository extends JpaRepository<Categorie, Long> {
List<Categorie> findBySaison(Saison saison); 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);
} }
@@ -117,8 +117,21 @@ public class CategorieService {
Dotation newDotation = new Dotation(); Dotation newDotation = new Dotation();
newDotation.setLicence(licence); newDotation.setLicence(licence);
newDotation.setEquipement(ce.getEquipement()); newDotation.setEquipement(ce.getEquipement());
newDotation.setChoisi(ce.getObligatoire()); // By default checked if obligatoire boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom());
boolean isNouvelleAndU11 = false;
if (licence.getTypeDemande() != null &&
(licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) &&
licence.getCategorie() != null &&
licence.getCategorie().isU11OrBelow()) {
isNouvelleAndU11 = true;
}
newDotation.setChoisi(ce.getObligatoire() || (isMaillot && isNouvelleAndU11)); // Checked if obligatoire or if it's a new registration for U11 or below
newDotation.setFourni(false); newDotation.setFourni(false);
newDotation.setTaille("");
newDotation.setNumero("");
if (licence.getAdherent() != null) {
newDotation.setFlocage(licence.getAdherent().getNom().toUpperCase() + " " + licence.getAdherent().getPrenom());
}
currentDotations.add(newDotation); currentDotations.add(newDotation);
} }
} }
@@ -24,7 +24,7 @@ public class DotationService {
sb.append('\ufeff'); sb.append('\ufeff');
// Header // 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) { for (Dotation d : dotations) {
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : ""; 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 poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : "";
String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : ""; String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : "";
String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : ""; 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 taille = d.getTaille() != null ? d.getTaille() : "";
String flocage = d.getFlocage() != null ? d.getFlocage() : ""; String flocage = d.getFlocage() != null ? d.getFlocage() : "";
String numero = d.getNumero() != null ? d.getNumero() : ""; String numero = d.getNumero() != null ? d.getNumero() : "";
@@ -43,6 +44,7 @@ public class DotationService {
.append(escapeCsv(poste)).append(";") .append(escapeCsv(poste)).append(";")
.append(escapeCsv(sexe)).append(";") .append(escapeCsv(sexe)).append(";")
.append(escapeCsv(equipement)).append(";") .append(escapeCsv(equipement)).append(";")
.append(escapeCsv(reference)).append(";")
.append(escapeCsv(taille)).append(";") .append(escapeCsv(taille)).append(";")
.append(escapeCsv(flocage)).append(";") .append(escapeCsv(flocage)).append(";")
.append(escapeCsv(numero)).append("\n"); .append(escapeCsv(numero)).append("\n");
@@ -57,7 +59,7 @@ public class DotationService {
sb.append('\ufeff'); sb.append('\ufeff');
// Header // 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) { for (Dotation d : dotations) {
String saison = d.getLicence().getSaison() != null ? d.getLicence().getSaison().getNom() : ""; 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 poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : "";
String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : ""; String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : "";
String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : ""; 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 taille = d.getTaille() != null ? d.getTaille() : "";
String flocage = d.getFlocage() != null ? d.getFlocage() : ""; String flocage = d.getFlocage() != null ? d.getFlocage() : "";
String numero = d.getNumero() != null ? d.getNumero() : ""; String numero = d.getNumero() != null ? d.getNumero() : "";
@@ -79,6 +82,7 @@ public class DotationService {
.append(escapeCsv(poste)).append(";") .append(escapeCsv(poste)).append(";")
.append(escapeCsv(sexe)).append(";") .append(escapeCsv(sexe)).append(";")
.append(escapeCsv(equipement)).append(";") .append(escapeCsv(equipement)).append(";")
.append(escapeCsv(reference)).append(";")
.append(escapeCsv(taille)).append(";") .append(escapeCsv(taille)).append(";")
.append(escapeCsv(flocage)).append(";") .append(escapeCsv(flocage)).append(";")
.append(escapeCsv(numero)).append(";") .append(escapeCsv(numero)).append(";")
@@ -42,7 +42,10 @@ public class PreInscriptionService {
adherent.setLieuNaissancePays(pre.getLieuNaissancePays()); adherent.setLieuNaissancePays(pre.getLieuNaissancePays());
adherent.setNationalite(pre.getNationalite()); adherent.setNationalite(pre.getNationalite());
adherent.setEmail(pre.getEmail()); adherent.setEmail(pre.getEmail());
adherent.setTelephone(pre.getTelephone());
adherent.setRepresentantLegal(pre.getRepresentantLegal()); adherent.setRepresentantLegal(pre.getRepresentantLegal());
adherent.setTypeDemande(pre.getTypeDemande());
adherent.setAncienClub(pre.getAncienClub());
// Par défaut // Par défaut
adherent.setSexe("MASCULIN"); adherent.setSexe("MASCULIN");
adherent.setTypeMaillot("JOUEUR"); adherent.setTypeMaillot("JOUEUR");
@@ -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);
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.3</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -1,14 +1,23 @@
package com.astalange.web.controller; package com.astalange.web.controller;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@ControllerAdvice @ControllerAdvice
public class GlobalControllerAdvice { public class GlobalControllerAdvice {
@Value("${app.version}")
private String appVersion;
@ModelAttribute("requestURI") @ModelAttribute("requestURI")
public String requestURI(final HttpServletRequest request) { public String requestURI(final HttpServletRequest request) {
return request.getRequestURI(); return request.getRequestURI();
} }
@ModelAttribute("appVersion")
public String appVersion() {
return appVersion;
}
} }
@@ -82,21 +82,8 @@ public class LicenceController {
licence = licenceRepository.save(licence); licence = licenceRepository.save(licence);
// Auto-initialize dotations for all configured equipments of the category // Auto-initialize dotations via CategorieService to ensure consistency
List<CategorieEquipement> catEquipements = categorie.getCategorieEquipements(); categorieService.syncDotationsForLicence(licence);
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);
}
}
return "redirect:/adherents/" + id + "/edit"; return "redirect:/adherents/" + id + "/edit";
} }
@@ -6,14 +6,20 @@ import com.astalange.core.entity.Saison;
import com.astalange.core.repository.PreInscriptionRepository; import com.astalange.core.repository.PreInscriptionRepository;
import com.astalange.core.repository.TokenPreInscriptionRepository; import com.astalange.core.repository.TokenPreInscriptionRepository;
import com.astalange.core.repository.SaisonRepository; import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.entity.Categorie;
import com.astalange.core.service.CaptchaValidationService; import com.astalange.core.service.CaptchaValidationService;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.beans.factory.annotation.Value;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import java.util.List;
import java.math.BigDecimal;
@Controller @Controller
@RequestMapping("/inscription-public") @RequestMapping("/inscription-public")
@@ -23,19 +29,26 @@ public class PublicInscriptionController {
private final TokenPreInscriptionRepository tokenRepository; private final TokenPreInscriptionRepository tokenRepository;
private final PreInscriptionRepository preInscriptionRepository; private final PreInscriptionRepository preInscriptionRepository;
private final SaisonRepository saisonRepository; private final SaisonRepository saisonRepository;
private final CategorieRepository categorieRepository;
public PublicInscriptionController(CaptchaValidationService captchaValidationService, public PublicInscriptionController(CaptchaValidationService captchaValidationService,
TokenPreInscriptionRepository tokenRepository, TokenPreInscriptionRepository tokenRepository,
PreInscriptionRepository preInscriptionRepository, PreInscriptionRepository preInscriptionRepository,
SaisonRepository saisonRepository) { SaisonRepository saisonRepository,
CategorieRepository categorieRepository) {
this.captchaValidationService = captchaValidationService; this.captchaValidationService = captchaValidationService;
this.tokenRepository = tokenRepository; this.tokenRepository = tokenRepository;
this.preInscriptionRepository = preInscriptionRepository; this.preInscriptionRepository = preInscriptionRepository;
this.saisonRepository = saisonRepository; this.saisonRepository = saisonRepository;
this.categorieRepository = categorieRepository;
} }
@Value("${captcha.sitekey:1x00000000000000000000AA}")
private String captchaSiteKey;
@GetMapping("/demarrer") @GetMapping("/demarrer")
public String showDemarrer(Model model) { public String showDemarrer(Model model) {
model.addAttribute("captchaSiteKey", captchaSiteKey);
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) { if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
model.addAttribute("inscriptionsFermees", true); model.addAttribute("inscriptionsFermees", true);
@@ -61,7 +74,7 @@ public class PublicInscriptionController {
TokenPreInscription token = new TokenPreInscription(); TokenPreInscription token = new TokenPreInscription();
token.setValeurUuid(UUID.randomUUID().toString()); token.setValeurUuid(UUID.randomUUID().toString());
token.setDateExpiration(LocalDateTime.now().plusMinutes(5)); token.setDateExpiration(LocalDateTime.now().plusMinutes(15));
tokenRepository.save(token); tokenRepository.save(token);
return "redirect:/inscription-public/formulaire?token=" + token.getValeurUuid(); return "redirect:/inscription-public/formulaire?token=" + token.getValeurUuid();
@@ -112,9 +125,69 @@ public class PublicInscriptionController {
token.setEstUtilise(true); token.setEstUtilise(true);
tokenRepository.save(token); 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 = BigDecimal.ZERO;
if (cat.isU11OrBelow()) {
prixMaillot = cat.getEquipementPrixByNom("Maillot");
}
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 didentité",
"Carte didentité ou passeport de l'adhérent",
"Livret de famille (si pas de pièce didentité)",
"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"; 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 = BigDecimal.ZERO;
if (cat.isU11OrBelow()) {
prixMaillot = cat.getEquipementPrixByNom("Maillot");
}
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
return tarifTotal.intValue() + "";
}
} catch (Exception e) {
// Return default on error
}
return "210 €";
}
@GetMapping("/succes") @GetMapping("/succes")
public String showSucces() { public String showSucces() {
return "public/succes"; return "public/succes";
@@ -20,3 +20,6 @@ spring:
server: server:
port: 8080 port: 8080
app:
version: @project.version@
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 173 KiB

@@ -117,6 +117,15 @@
th:classappend="${#fields.hasErrors('email')} ? 'border-red-500' : ''"> 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> <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="mt-1 text-xs text-red-600"></p>
</div> </div>
<!-- Téléphone -->
<div>
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
<input type="tel" id="telephone" th:field="*{telephone}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -139,6 +148,24 @@
</div> </div>
</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>
<!-- Removed Adresse --> <!-- Removed Adresse -->
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
@@ -540,7 +567,18 @@
form.action = '/adherents/' + adherentId + '/licences'; form.action = '/adherents/' + adherentId + '/licences';
document.getElementById('numeroLicenceInput').value = ''; document.getElementById('numeroLicenceInput').value = '';
document.getElementById('etatSelect').value = 'Brouillon'; document.getElementById('etatSelect').value = 'Brouillon';
document.getElementById('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 = ''; 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
@@ -51,6 +51,7 @@
<form th:action="@{/logout}" method="post"> <form th:action="@{/logout}" method="post">
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button> <button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
</form> </form>
<div class="mt-2 text-[10px] text-gray-400">v<span th:text="${appVersion}">1.0-SNAPSHOT</span></div>
</div> </div>
</aside> </aside>
</body> </body>
@@ -20,15 +20,21 @@
<!-- Header --> <!-- Header -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6"> <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> <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> </header>
<!-- Main section --> <!-- Main section -->
<div class="flex-1 overflow-auto p-6 bg-gray-50/50"> <div class="flex-1 overflow-auto p-6 bg-gray-50/50">
<!-- Message de bienvenue -->
<div class="flex items-center bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-8">
<img src="/images/logo.png" alt="Logo AS Talange" class="h-20 w-auto mr-6 object-contain drop-shadow-sm">
<div>
<h2 class="text-2xl font-bold text-gray-900">Bienvenue sur votre espace AS Talange</h2>
<p class="text-gray-500 mt-1">Retrouvez ci-dessous le résumé de l'activité du club.</p>
</div>
</div>
<!-- KPIs --> <!-- KPIs -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Total Adhérents --> <!-- Total Adhérents -->
@@ -14,6 +14,7 @@
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md border border-gray-100"> <div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md border border-gray-100">
<div class="text-center mb-8"> <div class="text-center mb-8">
<img src="/images/logo.png" alt="Logo AS Talange" class="h-24 w-auto mx-auto mb-4 object-contain drop-shadow-sm">
<h1 class="text-3xl font-bold text-gray-900 mb-2">AS Talange</h1> <h1 class="text-3xl font-bold text-gray-900 mb-2">AS Talange</h1>
<p class="text-gray-500">Outil de gestion administrative</p> <p class="text-gray-500">Outil de gestion administrative</p>
</div> </div>
@@ -27,6 +27,11 @@
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">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> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description (ex: Maillot domicile officiel)</label> <label class="block text-sm font-medium text-gray-700 mb-1">Description (ex: Maillot domicile officiel)</label>
<textarea th:field="*{description}" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea> <textarea th:field="*{description}" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
@@ -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">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">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">É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-left">Taille / Floc.</th>
<th class="py-3 px-4 font-medium text-center">Fourni</th> <th class="py-3 px-4 font-medium text-center">Fourni</th>
</tr> </tr>
@@ -78,6 +79,7 @@
</td> </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-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-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"> <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: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> <div th:if="${dotation.flocage != null && !dotation.flocage.isEmpty()}" th:text="'F: ' + ${dotation.flocage}">Flocage</div>
@@ -30,13 +30,15 @@
Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club. Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.
</div> </div>
<form th:unless="${inscriptionsFermees}" th:action="@{/inscription-public/demarrer}" method="post" <p th:unless="${inscriptionsFermees}" class="text-gray-600 mb-8 leading-relaxed">
class="flex flex-col items-center"> Afin de sécuriser le processus et d'éviter les soumissions automatisées (robots),
<!-- Remplacer par la vraie clé de site Cloudflare Turnstile --> nous vous demandons de bien vouloir valider le test de sécurité ci-dessous avant de continuer.
<div class="cf-turnstile mb-6" data-sitekey="1x00000000000000000000AA"></div> </p>
<button type="submit" <form th:unless="${inscriptionsFermees}" th:action="@{/inscription-public/demarrer}" method="post" class="flex flex-col items-center">
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors"> <div class="cf-turnstile mb-6" th:attr="data-sitekey=${captchaSiteKey}"></div>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-8 rounded-xl shadow-md hover:shadow-lg transition-all duration-200 transform hover:-translate-y-0.5">
Accéder au formulaire Accéder au formulaire
</button> </button>
</form> </form>
@@ -6,7 +6,17 @@
<title>AS Talange - Formulaire d'inscription</title> <title>AS Talange - Formulaire d'inscription</title>
<script src="https://cdn.tailwindcss.com"></script> <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"> <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> </head>
<body class="bg-gray-50 flex items-center justify-center min-h-screen py-12"> <body class="bg-gray-50 flex items-center justify-center min-h-screen py-12">
<div class="max-w-xl w-full bg-white rounded-xl shadow-md p-8 border border-gray-100"> <div class="max-w-xl w-full bg-white rounded-xl shadow-md p-8 border border-gray-100">
@@ -14,9 +24,50 @@
<h2 class="text-2xl font-bold text-gray-900 mb-2 text-center">Formulaire de pré-inscription</h2> <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> <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}" /> <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" 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 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 didentité</li>
<li>Carte didentité ou passeport de l'adhérent</li>
<li>Livret de famille (si pas de pièce didentité)</li>
<li>Adresse e-mail de l'adhérent (représentant légal en cas de mineur)</li>
<li>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 class="grid grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label> <label class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label>
@@ -56,10 +107,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"> <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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email <span class="text-red-500">*</span></label> <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"> <input type="email" th:field="*{email}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
<input type="tel" th:field="*{telephone}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<div class="flex items-start pt-2"> <div class="flex items-start pt-2">
<div class="flex items-center h-5"> <div class="flex items-center h-5">
@@ -78,10 +135,55 @@
<p class="text-[10px] text-gray-400 text-center mt-4 leading-tight"> <p class="text-[10px] text-gray-400 text-center mt-4 leading-tight">
Les données sont stockées de manière sécurisée, accessibles uniquement au bureau du club. Vous pouvez demander leur suppression à tout moment. Les données sont stockées de manière sécurisée, accessibles uniquement au bureau du club. Vous pouvez demander leur suppression à tout moment.
</p> </p>
<p class="text-[10px] text-gray-400 text-center mt-2">
v<span th:text="${appVersion}">1.0-SNAPSHOT</span>
</p>
</form> </form>
</div> </div>
<script> <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');
}
document.getElementById('dateNaissance').addEventListener('change', function() { document.getElementById('dateNaissance').addEventListener('change', function() {
const dateInput = this.value; const dateInput = this.value;
const repBlock = document.getElementById('representantBlock'); const repBlock = document.getElementById('representantBlock');
@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <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> <script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Inscription réussie</title> <title>Inscription réussie</title>
@@ -16,8 +17,56 @@
</svg> </svg>
</div> </div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">Pré-inscription envoyée !</h1> <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> <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> </div>
<h3 class="font-semibold text-yellow-900 mb-2 text-sm uppercase tracking-wide">Documents à 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> </body>
</html> </html>
+4
View File
@@ -10,6 +10,7 @@ services:
- "5433:5432" - "5433:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
restart: always
pgadmin: pgadmin:
image: dpage/pgadmin4 image: dpage/pgadmin4
@@ -21,6 +22,7 @@ services:
- "5050:80" - "5050:80"
depends_on: depends_on:
- db - db
restart: always
app: app:
build: build:
@@ -33,6 +35,8 @@ services:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/astalange - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/astalange
- SPRING_DATASOURCE_USERNAME=${POSTGRES_USER:-myuser} - SPRING_DATASOURCE_USERNAME=${POSTGRES_USER:-myuser}
- SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD:-mypassword} - SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD:-mypassword}
- CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY}
- CAPTCHA_SECRET=${CAPTCHA_SECRET}
depends_on: depends_on:
- db - db
restart: always restart: always
+1 -1
View File
@@ -13,7 +13,7 @@
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.3</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>as-talange-parent</name> <name>as-talange-parent</name>