60 Commits

Author SHA1 Message Date
ucef 19cee92437 feat: add active user session tracking and visualization in admin dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 4m1s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m34s
2026-07-03 17:18:14 +02:00
ucef 214ec08229 fix: resolve HTMX pagination bug on adherents list 2026-07-03 17:07:42 +02:00
ucef 62209ae659 fix(db): increase taille and numero length in dotation to 50
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m29s
2026-07-01 15:00:50 +02:00
ucef fc3373a1b4 feat(ui): ajout d'un tooltip sur les noms d'équipements tronqués dans la vue dotation
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m39s
2026-07-01 10:35:00 +02:00
ucef db3eb3ab9a feat: add sexe to pre-inscription form and entity 2026-07-01 09:32:42 +02:00
ucef d1f38f11f0 feat: add search filters to categories and equipements lists
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m16s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m23s
2026-06-30 15:11:16 +02:00
ucef acf9d6d301 fix: apply maillot price and default selection for all new registrations
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m0s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m19s
2026-06-30 14:42:59 +02:00
ucef 3066ff0e86 chore: bump version to 1.5-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m33s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m23s
2026-06-30 13:20:04 +02:00
ucef 64ad4be7dd chore: release v1.4
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m45s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m44s
2026-06-30 13:19:43 +02:00
ucef d30e795325 fix: harmoniser le titre des documents à fournir sur la page de succès
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m18s
2026-06-30 13:16:24 +02:00
ucef e0388c7884 revert: remove viewport meta tag to restore previous mobile display
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m21s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 1m40s
2026-06-30 12:45:37 +02:00
ucef 7cf29f6106 Merge remote-tracking branch 'origin/main'
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 10m47s
2026-06-29 16:30:07 +02:00
ucef bef01e3a0e feat: add viewport meta tag to all HTML templates for responsive mobile display 2026-06-29 16:29:58 +02:00
ucef 9cf1d26018 feat: add viewport meta tag to all HTML templates for responsive mobile display
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m6s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m13s
2026-06-29 15:44:48 +02:00
ucef 94d672c0f6 feat: handle goalkeeper vs field player equipment differentiation
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-29 14:54:36 +02:00
ucef 8156c8f3d0 feat: gestion dynamique des équipements dans la configuration d'une catégorie
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m10s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-29 11:46:15 +02:00
ucef 7249b45533 feat: ajout du numéro de chèque et du commentaire sur les paiements 2026-06-29 11:28:52 +02:00
ucef 7e036c1966 feat: paramétrage dynamique des dotations (sexe, tailles, couleurs) 2026-06-29 11:11:53 +02:00
ucef 194986f880 feat: pré-sélection de la catégorie par âge et sexe lors de la création d'une licence
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m18s
2026-06-29 09:28:38 +02:00
ucef d834b8d0d3 fix: ajout du script de migration Flyway pour les nouveaux champs
- Script V30__add_ancienne_categorie_and_sexe.sql pour ajouter les champs en DB.

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

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

- Synchronisation de l'ancienne catégorie entre la pré-inscription et la fiche adhérent.
2026-06-29 09:03:45 +02:00
ucef 348d6e6ccf chore: bump version to 1.4-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m17s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-25 11:05:23 +02:00
ucef e670e38343 feat: harmoniser les documents demandés sur la page de succès de pré-inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m26s
2026-06-25 10:34:41 +02:00
ucef fe3a1ec36c fix: forcer le retour visuel de validation sur mobile (novalidate)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m12s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m12s
2026-06-25 10:07:51 +02:00
ucef 44ae524626 ci: ajout d'une vérification de démarrage après le déploiement
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m47s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m22s
2026-06-25 09:47:54 +02:00
ucef 829a69f1a5 fix: corriger l'indentation YAML qui empêchait l'application de démarrer 2026-06-25 09:45:12 +02:00
ucef f8336fb407 feat: affichage du numéro de version de l'application
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 22s
2026-06-25 09:19:59 +02:00
ucef ab1094d29a feat: préciser le justificatif de domicile pour les adhérents nés à l'étranger 2026-06-25 09:12:21 +02:00
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
ucef 48c3220013 ci(gitea): utilisation de secrets dédiés pour la BDD de production
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m48s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Has been cancelled
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-22 23:52:47 +02:00
ucef f1bd5d9e08 ci(gitea): séparation des déploiements test et prod selon les branches 2026-06-22 23:25:01 +02:00
ucef 5737fb6429 ci(docker): injection des variables Captcha dans docker-compose 2026-06-22 23:25:01 +02:00
ucef 96eee4326a feat(security): configuration dynamique du Captcha Cloudflare Turnstile 2026-06-22 23:25:01 +02:00
ucef 14c83be77a feat(paiements): ajout d'un système d'audit log pour les paiements avec filtres et correction d'accès agent
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m46s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 2m48s
2026-06-22 22:44:14 +02:00
ucef 08dc39f61a fix(ui): élargissement des sections informations et licences adhérent 2026-06-22 22:44:14 +02:00
ucef 84265660b9 fix(ui): masquer les encadrés financiers du dashboard pour les agents de saisie 2026-06-22 22:44:14 +02:00
ucef da33ce934d fix(ui): dynamically display connected username in sidebar across all pages
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m15s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m14s
2026-06-15 23:13:06 +02:00
ucef e867acbb70 feat(licence): add licence search page with CSV export and reorganize sidebar menus
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m4s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Failing after 15s
2026-06-15 23:11:18 +02:00
ucef c4bb9803ed feat(equipements): clean equipment search, fix category regression, and sync dotations on category change 2026-06-15 23:11:05 +02:00
ucef e1cd5930fd feat(paiement): track payment manager (gestionnaire) in history 2026-06-15 23:09:46 +02:00
ucef d913579c77 feat(rgpd): add mandatory consent checkbox to public registration form 2026-06-15 23:09:37 +02:00
ucef 2adbdde121 chore: inject HTMX script in all templates to fix badge bugs 2026-06-15 23:09:23 +02:00
ucef bcf92487ab feat: Export CSV des commandes équipements (Dashboard)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m18s
- Ajout de la génération de CSV des équipements choisis et non fournis pour la saison active.
- Création du DotationService et ajout de la route /admin/equipements/export-commande.
- Ajout du bouton d'export sur le tableau de bord principal (index.html).
2026-06-14 01:53:16 +02:00
78 changed files with 2155 additions and 151 deletions
+77
View File
@@ -0,0 +1,77 @@
name: AS Talange CI/CD Pipeline - Production
on:
push:
branches:
- 'prod/*'
jobs:
test:
name: Build & Run Unit Tests
runs-on: [self-hosted, prod]
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_prod:
name: Deploy to Prod Environment
needs: test
if: startsWith(github.ref, 'refs/heads/prod/')
runs-on: [self-hosted, prod]
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:
PROD_DB_USER: ${{ secrets.PROD_DB_USER }}
PROD_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
CAPTCHA_SECRET: ${{ secrets.CAPTCHA_SECRET }}
CAPTCHA_SITEKEY: ${{ secrets.CAPTCHA_SITEKEY }}
run: |
echo "POSTGRES_USER=${PROD_DB_USER:-myuser}" > .env
echo "POSTGRES_PASSWORD=${PROD_DB_PASSWORD:-mypassword}" >> .env
echo "CAPTCHA_SECRET=${CAPTCHA_SECRET:-1x0000000000000000000000000000000AA}" >> .env
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
@@ -8,7 +8,7 @@ on:
jobs: jobs:
test: test:
name: Build & Run Unit Tests name: Build & Run Unit Tests
runs-on: self-hosted runs-on: [self-hosted, ubuntu-latest]
container: container:
image: maven:3.9.6-eclipse-temurin-21 image: maven:3.9.6-eclipse-temurin-21
steps: steps:
@@ -20,16 +20,17 @@ 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: deploy_test:
name: Build & Run in Docker Container name: Deploy to Test Environment
needs: test needs: test
runs-on: self-hosted if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, ubuntu-latest]
container: container:
image: docker:latest image: docker:latest
steps: steps:
@@ -39,7 +40,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
@@ -49,5 +50,24 @@ jobs:
run: | run: |
echo "POSTGRES_USER=${DB_USER:-myuser}" > .env echo "POSTGRES_USER=${DB_USER:-myuser}" > .env
echo "POSTGRES_PASSWORD=${DB_PASSWORD:-mypassword}" >> .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 down app || true
docker compose up -d --build app docker compose up -d --build app
- name: Verify application startup
run: |
echo "Waiting for application to start..."
COUNT=0
while [ $COUNT -lt 30 ]; do
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
echo "Application is up and login page is accessible!"
exit 0
fi
echo "Waiting... ($COUNT/30)"
sleep 5
COUNT=$((COUNT+1))
done
echo "Application failed to start or login page is not accessible."
docker logs astalange_app
exit 1
+3
View File
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -35,9 +35,21 @@ public class Adherent {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String email; private String email;
@Column(nullable = false, 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;
@Column(name = "ancienne_categorie", length = 50)
private String ancienneCategorie;
private boolean residentTalange = true; private boolean residentTalange = true;
@Column(nullable = false, length = 10) @Column(nullable = false, length = 10)
@@ -78,9 +90,21 @@ 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 String getAncienneCategorie() { return ancienneCategorie; }
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
public boolean isResidentTalange() { return residentTalange; } public boolean isResidentTalange() { return residentTalange; }
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; } public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
@@ -0,0 +1,104 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.math.BigDecimal;
@Entity
@Table(name = "audit_log_paiement")
public class AuditLogPaiement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String action; // CREATE, UPDATE, DELETE
@Column(nullable = false)
private LocalDateTime dateAction;
@Column(nullable = false)
private String utilisateur; // Username or 'Système'
@Column(nullable = true)
private Long paiementId; // Can be null if the payment is completely deleted or we just want to keep track
@Column(nullable = false, length = 1000)
private String details;
@Column(nullable = true)
private BigDecimal montant;
@Column(nullable = true)
private String adherentNomComplet;
@PrePersist
protected void onCreate() {
this.dateAction = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public LocalDateTime getDateAction() {
return dateAction;
}
public void setDateAction(LocalDateTime dateAction) {
this.dateAction = dateAction;
}
public String getUtilisateur() {
return utilisateur;
}
public void setUtilisateur(String utilisateur) {
this.utilisateur = utilisateur;
}
public Long getPaiementId() {
return paiementId;
}
public void setPaiementId(Long paiementId) {
this.paiementId = paiementId;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public BigDecimal getMontant() {
return montant;
}
public void setMontant(BigDecimal montant) {
this.montant = montant;
}
public String getAdherentNomComplet() {
return adherentNomComplet;
}
public void setAdherentNomComplet(String adherentNomComplet) {
this.adherentNomComplet = adherentNomComplet;
}
}
@@ -18,6 +18,9 @@ public class Categorie {
@Column(nullable = false, length = 20) @Column(nullable = false, length = 20)
private String nom; private String nom;
@Column(nullable = false, length = 10)
private String sexe = "MASCULIN";
@Column(name = "annee_min") @Column(name = "annee_min")
private Integer anneeMin; private Integer anneeMin;
@@ -45,6 +48,9 @@ public class Categorie {
public String getNom() { return nom; } public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; } public void setNom(String nom) { this.nom = nom; }
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public Integer getAnneeMin() { return anneeMin; } public Integer getAnneeMin() { return anneeMin; }
public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; } public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; }
@@ -84,4 +90,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;
}
} }
@@ -18,16 +18,19 @@ public class Dotation {
@JoinColumn(name = "equipement_id", nullable = false) @JoinColumn(name = "equipement_id", nullable = false)
private Equipement equipement; private Equipement equipement;
@Column(length = 10) @Column(length = 50)
private String taille; private String taille;
@Column(length = 50)
private String couleur;
@Column(length = 50) @Column(length = 50)
private String flocage; private String flocage;
@Column(nullable = false) @Column(nullable = false)
private Boolean fourni = false; private Boolean fourni = false;
@Column(length = 10) @Column(length = 50)
private String numero; private String numero;
@Column(nullable = false) @Column(nullable = false)
@@ -47,6 +50,9 @@ public class Dotation {
public String getTaille() { return taille; } public String getTaille() { return taille; }
public void setTaille(String taille) { this.taille = taille; } public void setTaille(String taille) { this.taille = taille; }
public String getCouleur() { return couleur; }
public void setCouleur(String couleur) { this.couleur = couleur; }
public String getFlocage() { return flocage; } public String getFlocage() { return flocage; }
public void setFlocage(String flocage) { this.flocage = flocage; } public void setFlocage(String flocage) { this.flocage = flocage; }
@@ -14,9 +14,24 @@ 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;
@Column(name = "gestion_sexe", nullable = false)
private boolean gestionSexe = false;
@Column(name = "type_public", nullable = false, length = 20)
private String typePublic = "TOUS"; // JOUEUR, GARDIEN, TOUS
@Column(name = "tailles_disponibles", length = 500)
private String taillesDisponibles;
@Column(name = "couleurs_disponibles", length = 500)
private String couleursDisponibles;
@ManyToOne(optional = false, fetch = FetchType.LAZY) @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false) @JoinColumn(name = "saison_id", nullable = false)
@@ -30,9 +45,24 @@ 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; }
public boolean isGestionSexe() { return gestionSexe; }
public void setGestionSexe(boolean gestionSexe) { this.gestionSexe = gestionSexe; }
public String getTypePublic() { return typePublic; }
public void setTypePublic(String typePublic) { this.typePublic = typePublic; }
public String getTaillesDisponibles() { return taillesDisponibles; }
public void setTaillesDisponibles(String taillesDisponibles) { this.taillesDisponibles = taillesDisponibles; }
public String getCouleursDisponibles() { return couleursDisponibles; }
public void setCouleursDisponibles(String couleursDisponibles) { this.couleursDisponibles = couleursDisponibles; }
public Saison getSaison() { return saison; } public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; } public void setSaison(Saison saison) { this.saison = saison; }
} }
@@ -26,6 +26,15 @@ public class Paiement {
@JoinColumn(name = "mode_paiement_id", nullable = false) @JoinColumn(name = "mode_paiement_id", nullable = false)
private ModePaiement modePaiement; private ModePaiement modePaiement;
@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 // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -42,4 +51,13 @@ public class Paiement {
public ModePaiement getModePaiement() { return modePaiement; } public ModePaiement getModePaiement() { return modePaiement; }
public void setModePaiement(ModePaiement modePaiement) { this.modePaiement = modePaiement; } public void setModePaiement(ModePaiement modePaiement) { this.modePaiement = modePaiement; }
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) @Column(nullable = false, length = 100)
private String prenom; private String prenom;
@Column(nullable = false, length = 10)
private String sexe;
@Column(name = "date_naissance", nullable = false) @Column(name = "date_naissance", nullable = false)
private LocalDate dateNaissance; private LocalDate dateNaissance;
@@ -33,6 +36,9 @@ public class PreInscription {
@Column(nullable = false, length = 150) @Column(nullable = false, length = 150)
private String email; private String email;
@Column(nullable = false, length = 20)
private String telephone;
@Column(name = "representant_legal", length = 150) @Column(name = "representant_legal", length = 150)
private String representantLegal; private String representantLegal;
@@ -46,6 +52,18 @@ public class PreInscription {
@JoinColumn(name = "saison_id", nullable = false) @JoinColumn(name = "saison_id", nullable = false)
private Saison saison; private Saison saison;
@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 // 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; }
@@ -56,6 +74,9 @@ public class PreInscription {
public String getPrenom() { return prenom; } public String getPrenom() { return prenom; }
public void setPrenom(String prenom) { this.prenom = prenom; } public void setPrenom(String prenom) { this.prenom = prenom; }
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public LocalDate getDateNaissance() { return dateNaissance; } public LocalDate getDateNaissance() { return dateNaissance; }
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; } public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
@@ -71,6 +92,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; }
@@ -82,4 +106,16 @@ public class PreInscription {
public Saison getSaison() { return saison; } public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; } public void setSaison(Saison saison) { this.saison = saison; }
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; }
} }
@@ -0,0 +1,26 @@
package com.astalange.core.repository;
import com.astalange.core.entity.AuditLogPaiement;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@Repository
public interface AuditLogPaiementRepository extends JpaRepository<AuditLogPaiement, Long> {
List<AuditLogPaiement> findAllByOrderByDateActionDesc();
@Query("SELECT a FROM AuditLogPaiement a WHERE " +
"(:action IS NULL OR :action = '' OR a.action = :action) AND " +
"(:utilisateur IS NULL OR :utilisateur = '' OR LOWER(a.utilisateur) LIKE LOWER(CONCAT('%', :utilisateur, '%'))) AND " +
"(:adherent IS NULL OR :adherent = '' OR LOWER(a.adherentNomComplet) LIKE LOWER(CONCAT('%', :adherent, '%'))) " +
"ORDER BY a.dateAction DESC")
List<AuditLogPaiement> findByFilters(
@Param("action") String action,
@Param("utilisateur") String utilisateur,
@Param("adherent") String adherent
);
}
@@ -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);
} }
@@ -1,9 +1,14 @@
package com.astalange.core.repository; package com.astalange.core.repository;
import com.astalange.core.entity.Dotation; import com.astalange.core.entity.Dotation;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import com.astalange.core.entity.Saison;
@Repository @Repository
public interface DotationRepository extends JpaRepository<Dotation, Long> { public interface DotationRepository extends JpaRepository<Dotation, Long>, JpaSpecificationExecutor<Dotation> {
List<Dotation> findByLicence_SaisonAndChoisiTrueAndFourniFalse(Saison saison);
} }
@@ -8,8 +8,10 @@ import org.springframework.stereotype.Repository;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository @Repository
public interface LicenceRepository extends JpaRepository<Licence, Long> { public interface LicenceRepository extends JpaRepository<Licence, Long>, JpaSpecificationExecutor<Licence> {
List<Licence> findByAdherentId(Long adherentId); List<Licence> findByAdherentId(Long adherentId);
List<Licence> findBySaisonAndCategorie(com.astalange.core.entity.Saison saison, com.astalange.core.entity.Categorie categorie); List<Licence> findBySaisonAndCategorie(com.astalange.core.entity.Saison saison, com.astalange.core.entity.Categorie categorie);
@@ -76,16 +76,31 @@ public class CategorieService {
} }
} }
private void updateDotationsForLicence(Licence licence, List<CategorieEquipement> categorieEquipements) { public void syncDotationsForLicence(Licence licence) {
if (licence.getCategorie() != null) {
updateDotationsForLicence(licence, licence.getCategorie().getCategorieEquipements());
}
}
public void updateDotationsForLicence(Licence licence, List<CategorieEquipement> categorieEquipements) {
List<Dotation> currentDotations = licence.getDotations(); List<Dotation> currentDotations = licence.getDotations();
List<Dotation> toRemove = new ArrayList<>(); List<Dotation> toRemove = new ArrayList<>();
// Remove dotations for equipements no longer in the category // Remove dotations for equipements no longer in the category or not matching public
for (Dotation d : currentDotations) { for (Dotation d : currentDotations) {
boolean found = false; boolean found = false;
for (CategorieEquipement ce : categorieEquipements) { for (CategorieEquipement ce : categorieEquipements) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) { if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
found = true; String typePublic = ce.getEquipement().getTypePublic();
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
boolean matchPublic = "TOUS".equals(typePublic) ||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
if (matchPublic) {
found = true;
}
break; break;
} }
} }
@@ -97,6 +112,17 @@ public class CategorieService {
// Add missing dotations // Add missing dotations
for (CategorieEquipement ce : categorieEquipements) { for (CategorieEquipement ce : categorieEquipements) {
String typePublic = ce.getEquipement().getTypePublic();
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
boolean matchPublic = "TOUS".equals(typePublic) ||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
if (!matchPublic) {
continue;
}
boolean found = false; boolean found = false;
for (Dotation d : currentDotations) { for (Dotation d : currentDotations) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) { if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
@@ -111,8 +137,20 @@ 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 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.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);
} }
} }
@@ -0,0 +1,108 @@
package com.astalange.core.service;
import com.astalange.core.entity.Dotation;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.DotationRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DotationService {
private final DotationRepository dotationRepository;
public DotationService(DotationRepository dotationRepository) {
this.dotationRepository = dotationRepository;
}
public String genererCsvCommandeEquipement(Saison saisonActive) {
List<Dotation> dotations = dotationRepository.findByLicence_SaisonAndChoisiTrueAndFourniFalse(saisonActive);
StringBuilder sb = new StringBuilder();
// BOM for Excel to open UTF-8 correctly
sb.append('\ufeff');
// Header
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() : "";
String nom = d.getLicence().getAdherent().getNom();
String prenom = d.getLicence().getAdherent().getPrenom();
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() : "";
sb.append(escapeCsv(categorie)).append(";")
.append(escapeCsv(nom)).append(";")
.append(escapeCsv(prenom)).append(";")
.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");
}
return sb.toString();
}
public String genererCsvSearchEquipement(List<Dotation> dotations) {
StringBuilder sb = new StringBuilder();
// BOM for Excel to open UTF-8 correctly
sb.append('\ufeff');
// Header
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() : "";
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : "";
String nom = d.getLicence().getAdherent().getNom();
String prenom = d.getLicence().getAdherent().getPrenom();
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() : "";
String fourni = Boolean.TRUE.equals(d.getFourni()) ? "Oui" : "Non";
sb.append(escapeCsv(saison)).append(";")
.append(escapeCsv(categorie)).append(";")
.append(escapeCsv(nom)).append(";")
.append(escapeCsv(prenom)).append(";")
.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(";")
.append(escapeCsv(fourni)).append("\n");
}
return sb.toString();
}
private String escapeCsv(String value) {
if (value == null) {
return "";
}
String result = value;
if (result.contains("\"")) {
result = result.replace("\"", "\"\"");
}
if (result.contains(";") || result.contains("\n") || result.contains("\"")) {
return "\"" + result + "\"";
}
return result;
}
}
@@ -42,9 +42,13 @@ 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());
adherent.setAncienneCategorie(pre.getAncienneCategorie());
adherent.setSexe(pre.getSexe());
// Par défaut // Par défaut
adherent.setSexe("MASCULIN");
adherent.setTypeMaillot("JOUEUR"); adherent.setTypeMaillot("JOUEUR");
adherent.setResidentTalange(false); adherent.setResidentTalange(false);
@@ -0,0 +1 @@
ALTER TABLE paiement ADD COLUMN gestionnaire VARCHAR(255);
@@ -0,0 +1 @@
ALTER TABLE pre_inscription ADD COLUMN consentement_rgpd BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,10 @@
CREATE TABLE audit_log_paiement (
id BIGSERIAL PRIMARY KEY,
action VARCHAR(50) NOT NULL,
date_action TIMESTAMP NOT NULL,
utilisateur VARCHAR(255) NOT NULL,
paiement_id BIGINT,
details VARCHAR(1000) NOT NULL,
montant DECIMAL(10, 2),
adherent_nom_complet VARCHAR(255)
);
@@ -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);
@@ -0,0 +1,10 @@
ALTER TABLE adherent ADD COLUMN ancienne_categorie VARCHAR(50);
ALTER TABLE pre_inscription ADD COLUMN ancienne_categorie VARCHAR(50) DEFAULT 'NA';
ALTER TABLE categorie ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
UPDATE adherent SET telephone = '0000000000' WHERE telephone IS NULL;
ALTER TABLE adherent ALTER COLUMN telephone SET NOT NULL;
UPDATE pre_inscription SET telephone = '0000000000' WHERE telephone IS NULL;
ALTER TABLE pre_inscription ALTER COLUMN telephone SET NOT NULL;
@@ -0,0 +1,5 @@
ALTER TABLE equipement ADD COLUMN gestion_sexe BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN tailles_disponibles VARCHAR(500);
ALTER TABLE equipement ADD COLUMN couleurs_disponibles VARCHAR(500);
ALTER TABLE dotation ADD COLUMN couleur VARCHAR(50);
@@ -0,0 +1,2 @@
ALTER TABLE paiement ADD COLUMN numero_cheque VARCHAR(50);
ALTER TABLE paiement ADD COLUMN commentaire TEXT;
@@ -0,0 +1 @@
ALTER TABLE equipement ADD COLUMN type_public VARCHAR(20) DEFAULT 'TOUS' NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE pre_inscription ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE dotation ALTER COLUMN taille TYPE VARCHAR(50);
ALTER TABLE dotation ALTER COLUMN numero TYPE VARCHAR(50);
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -10,6 +10,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -27,8 +30,8 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**", "/inscription-public/**").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**", "/inscription-public/**").permitAll()
.requestMatchers("/change-password").authenticated() .requestMatchers("/change-password").authenticated()
.requestMatchers("/utilisateurs/**", "/saisons/**", "/categories/**", "/equipes/**", "/modespaiement/**", "/equipements/**").hasRole("ADMIN") .requestMatchers("/utilisateurs/**", "/saisons/**", "/categories/**", "/equipes/**", "/modespaiement/**", "/equipements/**", "/admin/logs/**").hasRole("ADMIN")
.requestMatchers("/paiements/**", "/paiement/**").hasAnyRole("ADMIN", "TRESORERIE") .requestMatchers("/paiements/**", "/paiement/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE")
.requestMatchers("/planning/**").hasAnyRole("ADMIN", "AGENT_SAISIE") .requestMatchers("/planning/**").hasAnyRole("ADMIN", "AGENT_SAISIE")
.requestMatchers("/adherents/**", "/dotations/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE") .requestMatchers("/adherents/**", "/dotations/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE")
.anyRequest().authenticated() .anyRequest().authenticated()
@@ -41,6 +44,10 @@ public class SecurityConfig {
.logout(logout -> logout .logout(logout -> logout
.logoutSuccessUrl("/login?logout") .logoutSuccessUrl("/login?logout")
.permitAll() .permitAll()
)
.sessionManagement(session -> session
.maximumSessions(100)
.sessionRegistry(sessionRegistry())
); );
return http.build(); return http.build();
} }
@@ -57,4 +64,14 @@ public class SecurityConfig {
authProvider.setPasswordEncoder(passwordEncoder()); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; return authProvider;
} }
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
} }
@@ -23,13 +23,15 @@ public class AdherentController {
private final com.astalange.core.repository.LicenceRepository licenceRepository; private final com.astalange.core.repository.LicenceRepository licenceRepository;
private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository; private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository;
private final SaisonRepository saisonRepository; private final SaisonRepository saisonRepository;
private final com.astalange.core.service.CategorieService categorieService;
public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository) { public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository, com.astalange.core.service.CategorieService categorieService) {
this.adherentRepository = adherentRepository; this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository; this.licenceRepository = licenceRepository;
this.modePaiementRepository = modePaiementRepository; this.modePaiementRepository = modePaiementRepository;
this.saisonRepository = saisonRepository; this.saisonRepository = saisonRepository;
this.categorieService = categorieService;
} }
@org.springframework.web.bind.annotation.ModelAttribute("equipes") @org.springframework.web.bind.annotation.ModelAttribute("equipes")
@@ -175,20 +177,29 @@ public class AdherentController {
Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null); Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null);
if (existing != null) { if (existing != null) {
boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance()); boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance());
boolean typeMaillotChanged = !existing.getTypeMaillot().equals(adherent.getTypeMaillot());
if (dateChanged) { if (dateChanged || typeMaillotChanged) {
int newBirthYear = adherent.getDateNaissance().getYear(); int newBirthYear = adherent.getDateNaissance().getYear();
List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId()); List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId());
for (Licence licence : licences) { for (Licence licence : licences) {
Saison saison = licence.getSaison(); if (dateChanged) {
Categorie newCat = categorieRepository.findBySaison(saison).stream() Saison saison = licence.getSaison();
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax()) Categorie newCat = categorieRepository.findBySaison(saison).stream()
.findFirst() .filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.orElse(null); .findFirst()
if (newCat != null) { .orElse(null);
licence.setCategorie(newCat); if (newCat != null) {
licenceRepository.save(licence); licence.setCategorie(newCat);
licence = licenceRepository.save(licence);
}
} }
// We need to update the entity's state before syncing dotations so it reads the new typeMaillot.
// We haven't updated 'existing' yet, so let's temporarily set it on the licence's adherent or wait.
// Actually, 'adherent' has the new typeMaillot.
licence.getAdherent().setTypeMaillot(adherent.getTypeMaillot());
categorieService.syncDotationsForLicence(licence);
} }
} }
@@ -0,0 +1,34 @@
package com.astalange.web.controller;
import com.astalange.core.repository.AuditLogPaiementRepository;
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 org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/admin/logs/paiements")
public class AuditLogPaiementController {
private final AuditLogPaiementRepository auditLogPaiementRepository;
public AuditLogPaiementController(AuditLogPaiementRepository auditLogPaiementRepository) {
this.auditLogPaiementRepository = auditLogPaiementRepository;
}
@GetMapping
public String listLogs(
@RequestParam(required = false) String action,
@RequestParam(required = false) String utilisateur,
@RequestParam(required = false) String adherent,
Model model) {
model.addAttribute("logs", auditLogPaiementRepository.findByFilters(action, utilisateur, adherent));
model.addAttribute("paramAction", action);
model.addAttribute("paramUtilisateur", utilisateur);
model.addAttribute("paramAdherent", adherent);
return "audit/paiements_logs";
}
}
@@ -47,8 +47,13 @@ public class CategorieController {
Categorie categorie = categorieRepository.findById(id) Categorie categorie = categorieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id)); .orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
model.addAttribute("categorie", categorie); model.addAttribute("categorie", categorie);
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of()); Saison saison = categorie.getSaison();
if (saison == null) {
saison = saisonRepository.findByEstActiveTrue().orElse(null);
}
model.addAttribute("allEquipements", saison != null ? equipementRepository.findBySaison(saison) : List.of());
return "parametrage/categories_form"; return "parametrage/categories_form";
} }
@@ -11,13 +11,120 @@ import org.springframework.web.bind.annotation.RequestParam;
import java.util.List; import java.util.List;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import java.time.LocalDate;
import com.astalange.core.service.DotationService;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.repository.CategorieRepository;
import org.springframework.ui.Model;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
@Controller @Controller
public class DotationController { public class DotationController {
private final DotationRepository dotationRepository; private final DotationRepository dotationRepository;
private final DotationService dotationService;
private final SaisonRepository saisonRepository;
private final EquipementRepository equipementRepository;
private final CategorieRepository categorieRepository;
public DotationController(DotationRepository dotationRepository) { public DotationController(DotationRepository dotationRepository, DotationService dotationService, SaisonRepository saisonRepository, EquipementRepository equipementRepository, CategorieRepository categorieRepository) {
this.dotationRepository = dotationRepository; this.dotationRepository = dotationRepository;
this.dotationService = dotationService;
this.saisonRepository = saisonRepository;
this.equipementRepository = equipementRepository;
this.categorieRepository = categorieRepository;
}
private Specification<Dotation> buildSpecification(Long equipementId, Long categorieId, Boolean fourni, Saison saisonActive) {
return (root, query, cb) -> {
if (Long.class != query.getResultType() && long.class != query.getResultType()) {
root.fetch("equipement", jakarta.persistence.criteria.JoinType.LEFT);
jakarta.persistence.criteria.Fetch<Object, Object> licenceFetch = root.fetch("licence", jakarta.persistence.criteria.JoinType.LEFT);
licenceFetch.fetch("adherent", jakarta.persistence.criteria.JoinType.LEFT);
licenceFetch.fetch("categorie", jakarta.persistence.criteria.JoinType.LEFT);
}
List<Predicate> predicates = new ArrayList<>();
if (saisonActive != null) {
predicates.add(cb.equal(root.get("licence").get("saison"), saisonActive));
}
if (equipementId != null) {
predicates.add(cb.equal(root.get("equipement").get("id"), equipementId));
}
if (categorieId != null) {
predicates.add(cb.equal(root.get("licence").get("categorie").get("id"), categorieId));
}
if (fourni != null) {
predicates.add(cb.equal(root.get("fourni"), fourni));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
@GetMapping("/admin/equipements/recherche")
public String rechercheEquipements(
@RequestParam(required = false) Long equipementId,
@RequestParam(required = false) Long categorieId,
@RequestParam(required = false) Boolean fourni,
Model model) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
Specification<Dotation> spec = buildSpecification(equipementId, categorieId, fourni, saisonActive);
List<Dotation> dotations = dotationRepository.findAll(spec);
model.addAttribute("dotations", dotations);
model.addAttribute("equipements", equipementRepository.findAll());
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("equipementId", equipementId);
model.addAttribute("categorieId", categorieId);
model.addAttribute("fourni", fourni);
return "parametrage/equipements_recherche";
}
@GetMapping("/admin/equipements/recherche/export")
public ResponseEntity<byte[]> exportRechercheEquipements(
@RequestParam(required = false) Long equipementId,
@RequestParam(required = false) Long categorieId,
@RequestParam(required = false) Boolean fourni) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
Specification<Dotation> spec = buildSpecification(equipementId, categorieId, fourni, saisonActive);
List<Dotation> dotations = dotationRepository.findAll(spec);
String csvContent = dotationService.genererCsvSearchEquipement(dotations);
byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8);
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "recherche_equipements_" + LocalDate.now() + ".csv");
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
}
@GetMapping("/admin/equipements/export-commande")
public ResponseEntity<byte[]> exportCommande() {
Saison saisonActive = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active"));
String csvContent = dotationService.genererCsvCommandeEquipement(saisonActive);
byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8);
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "commande_equipements_" + LocalDate.now() + ".csv");
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
} }
@PostMapping("/dotations/{id}") @PostMapping("/dotations/{id}")
@@ -61,12 +168,14 @@ public class DotationController {
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id)); .orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
String taille = request.getParameter("taille_" + id); String taille = request.getParameter("taille_" + id);
String couleur = request.getParameter("couleur_" + id);
String numero = request.getParameter("numero_" + id); String numero = request.getParameter("numero_" + id);
String flocage = request.getParameter("flocage_" + id); String flocage = request.getParameter("flocage_" + id);
String choisiParam = request.getParameter("choisi_" + id); String choisiParam = request.getParameter("choisi_" + id);
String fourniParam = request.getParameter("fourni_" + id); String fourniParam = request.getParameter("fourni_" + id);
if (taille != null) dotation.setTaille(taille); if (taille != null) dotation.setTaille(taille);
if (couleur != null) dotation.setCouleur(couleur);
if (numero != null) dotation.setNumero(numero); if (numero != null) dotation.setNumero(numero);
if (flocage != null) dotation.setFlocage(flocage); if (flocage != null) dotation.setFlocage(flocage);
@@ -22,7 +22,12 @@ public class EquipementController {
@GetMapping @GetMapping
public String listEquipements(Model model) { public String listEquipements(Model model) {
model.addAttribute("equipements", equipementRepository.findAll()); Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison != null) {
model.addAttribute("equipements", equipementRepository.findBySaison(activeSaison));
} else {
model.addAttribute("equipements", java.util.List.of());
}
return "parametrage/equipements_list"; return "parametrage/equipements_list";
} }
@@ -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;
}
} }
@@ -21,6 +21,7 @@ public class LicenceController {
private final DotationRepository dotationRepository; private final DotationRepository dotationRepository;
private final SaisonRepository saisonRepository; private final SaisonRepository saisonRepository;
private final EquipeRepository equipeRepository; private final EquipeRepository equipeRepository;
private final com.astalange.core.service.CategorieService categorieService;
public LicenceController(AdherentRepository adherentRepository, public LicenceController(AdherentRepository adherentRepository,
CategorieRepository categorieRepository, CategorieRepository categorieRepository,
@@ -28,7 +29,8 @@ public class LicenceController {
EquipementRepository equipementRepository, EquipementRepository equipementRepository,
DotationRepository dotationRepository, DotationRepository dotationRepository,
SaisonRepository saisonRepository, SaisonRepository saisonRepository,
EquipeRepository equipeRepository) { EquipeRepository equipeRepository,
com.astalange.core.service.CategorieService categorieService) {
this.adherentRepository = adherentRepository; this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository; this.licenceRepository = licenceRepository;
@@ -36,6 +38,7 @@ public class LicenceController {
this.dotationRepository = dotationRepository; this.dotationRepository = dotationRepository;
this.saisonRepository = saisonRepository; this.saisonRepository = saisonRepository;
this.equipeRepository = equipeRepository; this.equipeRepository = equipeRepository;
this.categorieService = categorieService;
} }
@PostMapping("/adherents/{id}/licences") @PostMapping("/adherents/{id}/licences")
@@ -79,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";
} }
@@ -129,8 +119,125 @@ public class LicenceController {
licence.setEquipe(null); licence.setEquipe(null);
} }
licenceRepository.save(licence); licence = licenceRepository.save(licence);
// Sync dotations when category changes
categorieService.syncDotationsForLicence(licence);
return "redirect:/adherents/" + adherentId + "/edit"; return "redirect:/adherents/" + adherentId + "/edit";
} }
@org.springframework.web.bind.annotation.GetMapping("/admin/licences/recherche")
public String rechercheLicences(
@RequestParam(required = false) String nom,
@RequestParam(required = false) String prenom,
@RequestParam(required = false) Long categorieId,
@RequestParam(required = false) String numeroLicence,
@RequestParam(required = false) String email,
@RequestParam(required = false) String typeDemande,
org.springframework.ui.Model model) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
org.springframework.data.jpa.domain.Specification<Licence> spec = buildLicenceSpecification(nom, prenom, categorieId, numeroLicence, email, typeDemande, saisonActive);
List<Licence> licences = licenceRepository.findAll(spec);
model.addAttribute("licences", licences);
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("nomFilter", nom);
model.addAttribute("prenomFilter", prenom);
model.addAttribute("categorieId", categorieId);
model.addAttribute("numeroLicenceFilter", numeroLicence);
model.addAttribute("emailFilter", email);
model.addAttribute("typeDemandeFilter", typeDemande);
return "parametrage/licences_recherche";
}
@org.springframework.web.bind.annotation.GetMapping("/admin/licences/recherche/export")
public org.springframework.http.ResponseEntity<byte[]> exportRechercheLicences(
@RequestParam(required = false) String nom,
@RequestParam(required = false) String prenom,
@RequestParam(required = false) Long categorieId,
@RequestParam(required = false) String numeroLicence,
@RequestParam(required = false) String email,
@RequestParam(required = false) String typeDemande) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
org.springframework.data.jpa.domain.Specification<Licence> spec = buildLicenceSpecification(nom, prenom, categorieId, numeroLicence, email, typeDemande, saisonActive);
List<Licence> licences = licenceRepository.findAll(spec);
StringBuilder sb = new StringBuilder();
sb.append('\ufeff');
sb.append("Nom;Prénom;Catégorie;N° Licence;Email;Type de Demande;Saison;Etat\n");
for (Licence l : licences) {
String n = l.getAdherent() != null && l.getAdherent().getNom() != null ? l.getAdherent().getNom() : "";
String p = l.getAdherent() != null && l.getAdherent().getPrenom() != null ? l.getAdherent().getPrenom() : "";
String c = l.getCategorie() != null ? l.getCategorie().getNom() : "";
String num = l.getNumeroLicence() != null ? l.getNumeroLicence() : "";
String e = l.getAdherent() != null && l.getAdherent().getEmail() != null ? l.getAdherent().getEmail() : "";
String td = l.getTypeDemande() != null ? l.getTypeDemande() : "";
String s = l.getSaison() != null ? l.getSaison().getNom() : "";
String etat = l.getEtat() != null ? l.getEtat() : "";
sb.append(escapeCsv(n)).append(";")
.append(escapeCsv(p)).append(";")
.append(escapeCsv(c)).append(";")
.append(escapeCsv(num)).append(";")
.append(escapeCsv(e)).append(";")
.append(escapeCsv(td)).append(";")
.append(escapeCsv(s)).append(";")
.append(escapeCsv(etat)).append("\n");
}
byte[] csvBytes = sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.setContentDispositionFormData("attachment", "recherche_licences_" + java.time.LocalDate.now() + ".csv");
headers.setContentType(org.springframework.http.MediaType.parseMediaType("text/csv; charset=UTF-8"));
return new org.springframework.http.ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
}
private String escapeCsv(String value) {
if (value == null) return "";
if (value.contains("\"")) value = value.replace("\"", "\"\"");
if (value.contains(";") || value.contains("\n") || value.contains("\"")) return "\"" + value + "\"";
return value;
}
private org.springframework.data.jpa.domain.Specification<Licence> buildLicenceSpecification(
String nom, String prenom, Long categorieId, String numeroLicence, String email, String typeDemande, Saison saisonActive) {
return (root, query, cb) -> {
List<jakarta.persistence.criteria.Predicate> predicates = new java.util.ArrayList<>();
if (Long.class != query.getResultType() && long.class != query.getResultType()) {
root.fetch("adherent", jakarta.persistence.criteria.JoinType.LEFT);
root.fetch("categorie", jakarta.persistence.criteria.JoinType.LEFT);
}
if (saisonActive != null) {
predicates.add(cb.equal(root.get("saison"), saisonActive));
}
if (nom != null && !nom.trim().isEmpty()) {
predicates.add(cb.like(cb.lower(root.get("adherent").get("nom")), "%" + nom.trim().toLowerCase() + "%"));
}
if (prenom != null && !prenom.trim().isEmpty()) {
predicates.add(cb.like(cb.lower(root.get("adherent").get("prenom")), "%" + prenom.trim().toLowerCase() + "%"));
}
if (categorieId != null) {
predicates.add(cb.equal(root.get("categorie").get("id"), categorieId));
}
if (numeroLicence != null && !numeroLicence.trim().isEmpty()) {
predicates.add(cb.like(cb.lower(root.get("numeroLicence")), "%" + numeroLicence.trim().toLowerCase() + "%"));
}
if (email != null && !email.trim().isEmpty()) {
predicates.add(cb.like(cb.lower(root.get("adherent").get("email")), "%" + email.trim().toLowerCase() + "%"));
}
if (typeDemande != null && !typeDemande.trim().isEmpty()) {
predicates.add(cb.equal(root.get("typeDemande"), typeDemande.trim()));
}
return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
};
}
} }
@@ -3,6 +3,8 @@ package com.astalange.web.controller;
import com.astalange.core.entity.Licence; import com.astalange.core.entity.Licence;
import com.astalange.core.entity.ModePaiement; import com.astalange.core.entity.ModePaiement;
import com.astalange.core.entity.Paiement; import com.astalange.core.entity.Paiement;
import com.astalange.core.entity.AuditLogPaiement;
import com.astalange.core.repository.AuditLogPaiementRepository;
import com.astalange.core.repository.LicenceRepository; import com.astalange.core.repository.LicenceRepository;
import com.astalange.core.repository.ModePaiementRepository; import com.astalange.core.repository.ModePaiementRepository;
import com.astalange.core.repository.PaiementRepository; import com.astalange.core.repository.PaiementRepository;
@@ -11,6 +13,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import java.security.Principal;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
@@ -21,11 +24,13 @@ public class PaiementController {
private final LicenceRepository licenceRepository; private final LicenceRepository licenceRepository;
private final ModePaiementRepository modePaiementRepository; private final ModePaiementRepository modePaiementRepository;
private final PaiementRepository paiementRepository; private final PaiementRepository paiementRepository;
private final AuditLogPaiementRepository auditLogPaiementRepository;
public PaiementController(LicenceRepository licenceRepository, ModePaiementRepository modePaiementRepository, PaiementRepository paiementRepository) { public PaiementController(LicenceRepository licenceRepository, ModePaiementRepository modePaiementRepository, PaiementRepository paiementRepository, AuditLogPaiementRepository auditLogPaiementRepository) {
this.licenceRepository = licenceRepository; this.licenceRepository = licenceRepository;
this.modePaiementRepository = modePaiementRepository; this.modePaiementRepository = modePaiementRepository;
this.paiementRepository = paiementRepository; this.paiementRepository = paiementRepository;
this.auditLogPaiementRepository = auditLogPaiementRepository;
} }
@PostMapping("/licences/{id}/paiements") @PostMapping("/licences/{id}/paiements")
@@ -34,7 +39,10 @@ public class PaiementController {
@RequestParam Long adherentId, @RequestParam Long adherentId,
@RequestParam BigDecimal montant, @RequestParam BigDecimal montant,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
@RequestParam Long modePaiementId) { @RequestParam Long modePaiementId,
@RequestParam(required = false) String numeroCheque,
@RequestParam(required = false) String commentaire,
Principal principal) {
Licence licence = licenceRepository.findById(id) Licence licence = licenceRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID")); .orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
@@ -52,7 +60,21 @@ public class PaiementController {
paiement.setModePaiement(mode); paiement.setModePaiement(mode);
paiement.setMontant(montant); paiement.setMontant(montant);
paiement.setDatePaiement(datePaiement); paiement.setDatePaiement(datePaiement);
paiement.setNumeroCheque(numeroCheque);
paiement.setCommentaire(commentaire);
if (principal != null) {
paiement.setGestionnaire(principal.getName());
}
paiementRepository.save(paiement); paiementRepository.save(paiement);
AuditLogPaiement log = new AuditLogPaiement();
log.setAction("CREATE");
log.setUtilisateur(principal != null ? principal.getName() : "Système");
log.setPaiementId(paiement.getId());
log.setMontant(montant);
log.setAdherentNomComplet(licence.getAdherent().getNom() + " " + licence.getAdherent().getPrenom());
log.setDetails("Création d'un paiement de " + montant + "€ via " + mode.getNom() + " pour la licence " + (licence.getNumeroLicence() != null ? licence.getNumeroLicence() : "sans numéro"));
auditLogPaiementRepository.save(log);
} }
return "redirect:/adherents/" + adherentId + "/edit"; return "redirect:/adherents/" + adherentId + "/edit";
@@ -64,7 +86,10 @@ public class PaiementController {
@RequestParam BigDecimal montant, @RequestParam BigDecimal montant,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
@RequestParam Long modePaiementId, @RequestParam Long modePaiementId,
@RequestParam(required = false) String redirect) { @RequestParam(required = false) String numeroCheque,
@RequestParam(required = false) String commentaire,
@RequestParam(required = false) String redirect,
Principal principal) {
Paiement paiement = paiementRepository.findById(id) Paiement paiement = paiementRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid Paiement ID")); .orElseThrow(() -> new IllegalArgumentException("Invalid Paiement ID"));
@@ -72,6 +97,9 @@ public class PaiementController {
ModePaiement mode = modePaiementRepository.findById(modePaiementId) ModePaiement mode = modePaiementRepository.findById(modePaiementId)
.orElseThrow(() -> new IllegalArgumentException("Invalid ModePaiement ID")); .orElseThrow(() -> new IllegalArgumentException("Invalid ModePaiement ID"));
BigDecimal ancienMontant = paiement.getMontant();
String ancienMode = paiement.getModePaiement().getNom();
Licence licence = paiement.getLicence(); Licence licence = paiement.getLicence();
// Validation basique pour ne pas dépasser le montant total de la licence // Validation basique pour ne pas dépasser le montant total de la licence
@@ -84,7 +112,21 @@ public class PaiementController {
paiement.setModePaiement(mode); paiement.setModePaiement(mode);
paiement.setMontant(montant); paiement.setMontant(montant);
paiement.setDatePaiement(datePaiement); paiement.setDatePaiement(datePaiement);
paiement.setNumeroCheque(numeroCheque);
paiement.setCommentaire(commentaire);
if (principal != null) {
paiement.setGestionnaire(principal.getName());
}
paiementRepository.save(paiement); paiementRepository.save(paiement);
AuditLogPaiement log = new AuditLogPaiement();
log.setAction("UPDATE");
log.setUtilisateur(principal != null ? principal.getName() : "Système");
log.setPaiementId(paiement.getId());
log.setMontant(montant);
log.setAdherentNomComplet(licence.getAdherent().getNom() + " " + licence.getAdherent().getPrenom());
log.setDetails("Modification du paiement: montant " + ancienMontant + "€ -> " + montant + "€, mode " + ancienMode + " -> " + mode.getNom());
auditLogPaiementRepository.save(log);
} }
if (redirect != null && !redirect.isEmpty()) { if (redirect != null && !redirect.isEmpty()) {
@@ -96,12 +138,23 @@ public class PaiementController {
@PostMapping("/paiements/{id}/delete") @PostMapping("/paiements/{id}/delete")
public String deletePaiement( public String deletePaiement(
@PathVariable Long id, @PathVariable Long id,
@RequestParam(required = false) String redirect) { @RequestParam(required = false) String redirect,
Principal principal) {
Paiement paiement = paiementRepository.findById(id) Paiement paiement = paiementRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid Paiement ID")); .orElseThrow(() -> new IllegalArgumentException("Invalid Paiement ID"));
Long adherentId = paiement.getLicence().getAdherent().getId(); Long adherentId = paiement.getLicence().getAdherent().getId();
AuditLogPaiement log = new AuditLogPaiement();
log.setAction("DELETE");
log.setUtilisateur(principal != null ? principal.getName() : "Système");
log.setPaiementId(paiement.getId());
log.setMontant(paiement.getMontant());
log.setAdherentNomComplet(paiement.getLicence().getAdherent().getNom() + " " + paiement.getLicence().getAdherent().getPrenom());
log.setDetails("Suppression du paiement de " + paiement.getMontant() + "€ via " + paiement.getModePaiement().getNom());
auditLogPaiementRepository.save(log);
paiementRepository.delete(paiement); paiementRepository.delete(paiement);
if (redirect != null && !redirect.isEmpty()) { if (redirect != null && !redirect.isEmpty()) {
@@ -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);
@@ -44,13 +57,16 @@ public class PublicInscriptionController {
} }
@PostMapping("/demarrer") @PostMapping("/demarrer")
public String processDemarrer(@RequestParam("cf-turnstile-response") String captchaResponse, RedirectAttributes redirectAttributes) { public String processDemarrer(@RequestParam("cf-turnstile-response") String captchaResponse,
RedirectAttributes redirectAttributes) {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) { if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club."); redirectAttributes.addFlashAttribute("error",
"Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
return "redirect:/inscription-public/demarrer"; return "redirect:/inscription-public/demarrer";
} }
// En vrai, captchaResponse dépend du fournisseur. Cloudflare Turnstile = cf-turnstile-response // En vrai, captchaResponse dépend du fournisseur. Cloudflare Turnstile =
// cf-turnstile-response
if (!captchaValidationService.validateCaptcha(captchaResponse)) { if (!captchaValidationService.validateCaptcha(captchaResponse)) {
redirectAttributes.addFlashAttribute("error", "Validation CAPTCHA échouée. Veuillez réessayer."); redirectAttributes.addFlashAttribute("error", "Validation CAPTCHA échouée. Veuillez réessayer.");
return "redirect:/inscription-public/demarrer"; return "redirect:/inscription-public/demarrer";
@@ -58,20 +74,22 @@ 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();
} }
@GetMapping("/formulaire") @GetMapping("/formulaire")
public String showFormulaire(@RequestParam("token") String tokenUuid, Model model, RedirectAttributes redirectAttributes) { public String showFormulaire(@RequestParam("token") String tokenUuid, Model model,
RedirectAttributes redirectAttributes) {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) { if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club."); redirectAttributes.addFlashAttribute("error",
"Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
return "redirect:/inscription-public/demarrer"; return "redirect:/inscription-public/demarrer";
} }
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null); TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
if (token == null || !token.isValide()) { if (token == null || !token.isValide()) {
return "public/lien-expire"; return "public/lien-expire";
@@ -84,8 +102,8 @@ public class PublicInscriptionController {
@PostMapping("/formulaire") @PostMapping("/formulaire")
public String processFormulaire(@RequestParam("token") String tokenUuid, public String processFormulaire(@RequestParam("token") String tokenUuid,
@ModelAttribute("preInscription") PreInscription preInscription, @ModelAttribute("preInscription") PreInscription preInscription,
RedirectAttributes redirectAttributes) { RedirectAttributes redirectAttributes) {
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null); TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
if (token == null || !token.isValide()) { if (token == null || !token.isValide()) {
return "public/lien-expire"; return "public/lien-expire";
@@ -93,7 +111,8 @@ public class PublicInscriptionController {
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null); Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) { if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
redirectAttributes.addFlashAttribute("error", "Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club."); redirectAttributes.addFlashAttribute("error",
"Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
return "redirect:/inscription-public/demarrer"; return "redirect:/inscription-public/demarrer";
} }
@@ -106,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 = 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 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 = 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") @GetMapping("/succes")
public String showSucces() { public String showSucces() {
return "public/succes"; 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: 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

@@ -25,7 +25,7 @@
<!-- Main section --> <!-- Main section -->
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8"> <div class="max-w-6xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<form th:action="@{/adherents}" th:object="${adherent}" method="post" class="space-y-6"> <form th:action="@{/adherents}" th:object="${adherent}" method="post" class="space-y-6">
<input type="hidden" th:field="*{id}" /> <input type="hidden" th:field="*{id}" />
@@ -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 <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>
<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,47 @@
</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>
<div id="ancienneCategorieBlock" class="grid grid-cols-1 md:grid-cols-2 gap-6 hidden-block mt-6">
<div>
<label for="ancienneCategorie" class="block text-sm font-medium text-gray-700 mb-1">Ancienne catégorie <span class="text-red-500">*</span></label>
<select id="ancienneCategorie" th:field="*{ancienneCategorie}"
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
<option value="NA">NA (Je ne sais plus)</option>
<option value="U6">U6</option>
<option value="U7">U7</option>
<option value="U8">U8</option>
<option value="U9">U9</option>
<option value="U10">U10</option>
<option value="U11">U11</option>
<option value="U12">U12</option>
<option value="U13">U13</option>
<option value="U14">U14</option>
<option value="U15">U15</option>
<option value="U16">U16</option>
<option value="U17">U17</option>
<option value="Senior">Senior</option>
</select>
</div>
</div>
<!-- Removed Adresse --> <!-- Removed Adresse -->
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
@@ -149,7 +199,7 @@
</div> </div>
<!-- Licences Section (Only visible for existing adherents) --> <!-- Licences Section (Only visible for existing adherents) -->
<div th:if="${adherent.id != null}" class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-6"> <div th:if="${adherent.id != null}" class="max-w-6xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-6">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Licences de l'adhérent</h3> <h3 class="text-xl font-bold text-gray-900">Licences de l'adhérent</h3>
<button th:if="${#lists.isEmpty(licences)}" onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors"> <button th:if="${#lists.isEmpty(licences)}" onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
@@ -228,6 +278,8 @@
<tr class="bg-gray-50 text-gray-500 uppercase font-medium"> <tr class="bg-gray-50 text-gray-500 uppercase font-medium">
<th class="py-2 px-3 text-left">Date</th> <th class="py-2 px-3 text-left">Date</th>
<th class="py-2 px-3 text-left">Mode</th> <th class="py-2 px-3 text-left">Mode</th>
<th class="py-2 px-3 text-left">Infos</th>
<th class="py-2 px-3 text-left">Géré par</th>
<th class="py-2 px-3 text-right font-medium">Montant</th> <th class="py-2 px-3 text-right font-medium">Montant</th>
<th class="py-2 px-3 text-right font-medium pr-4">Actions</th> <th class="py-2 px-3 text-right font-medium pr-4">Actions</th>
</tr> </tr>
@@ -238,6 +290,15 @@
<td class="py-2 px-3 text-gray-600"> <td class="py-2 px-3 text-gray-600">
<span class="px-2 py-0.5 rounded bg-gray-100 text-gray-700 font-mono text-[10px]" th:text="${paiement.modePaiement.nom}">Chèque</span> <span class="px-2 py-0.5 rounded bg-gray-100 text-gray-700 font-mono text-[10px]" th:text="${paiement.modePaiement.nom}">Chèque</span>
</td> </td>
<td class="py-2 px-3 text-gray-500 text-xs">
<div th:if="${paiement.numeroCheque != null and !paiement.numeroCheque.isEmpty()}" class="text-[10px]">
<span class="font-semibold text-gray-600">N°:</span> <span th:text="${paiement.numeroCheque}">123</span>
</div>
<div th:if="${paiement.commentaire != null and !paiement.commentaire.isEmpty()}" class="text-[10px] italic mt-0.5 text-gray-400 truncate max-w-[120px]" th:title="${paiement.commentaire}" th:text="${paiement.commentaire}">
Commentaire
</div>
</td>
<td class="py-2 px-3 text-gray-500 text-xs italic" th:text="${paiement.gestionnaire != null ? paiement.gestionnaire : '-'}">-</td>
<td class="py-2 px-3 text-right font-semibold text-green-600" th:text="${paiement.montant + ' €'}">50.00 €</td> <td class="py-2 px-3 text-right font-semibold text-green-600" th:text="${paiement.montant + ' €'}">50.00 €</td>
<td class="py-2 px-3 text-right pr-4"> <td class="py-2 px-3 text-right pr-4">
<div class="flex justify-end items-center space-x-2"> <div class="flex justify-end items-center space-x-2">
@@ -248,7 +309,9 @@
th:data-mode-id="${paiement.modePaiement.id}" th:data-mode-id="${paiement.modePaiement.id}"
th:data-licence-id="${lic.id}" th:data-licence-id="${lic.id}"
th:data-max-amount="${lic.getResteAPayer().add(paiement.montant)}" th:data-max-amount="${lic.getResteAPayer().add(paiement.montant)}"
onclick="openEditPaiementModal(this.getAttribute('data-paiement-id'), this.getAttribute('data-montant'), this.getAttribute('data-date'), this.getAttribute('data-mode-id'), this.getAttribute('data-licence-id'), this.getAttribute('data-max-amount'))" th:data-cheque="${paiement.numeroCheque}"
th:data-commentaire="${paiement.commentaire}"
onclick="openEditPaiementModal(this.getAttribute('data-paiement-id'), this.getAttribute('data-montant'), this.getAttribute('data-date'), this.getAttribute('data-mode-id'), this.getAttribute('data-licence-id'), this.getAttribute('data-max-amount'), this.getAttribute('data-cheque'), this.getAttribute('data-commentaire'))"
class="text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 p-1.5 rounded transition-colors inline-flex items-center" class="text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 p-1.5 rounded transition-colors inline-flex items-center"
title="Modifier"> title="Modifier">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -295,24 +358,17 @@
<div> <div>
<div class="flex justify-between items-start mb-1 gap-1"> <div class="flex justify-between items-start mb-1 gap-1">
<span class="font-semibold text-xs text-gray-800 truncate" th:text="${dot.equipement.nom}">Maillot</span> <span class="font-semibold text-xs text-gray-800 truncate" th:title="${dot.equipement.nom}" th:text="${dot.equipement.nom}">Maillot</span>
<span class="text-[9px] px-1.5 py-0.5 rounded-full font-bold flex-shrink-0" <span class="text-[9px] px-1.5 py-0.5 rounded-full font-bold flex-shrink-0"
th:classappend="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'bg-red-50 text-red-700 border border-red-100' : 'bg-gray-50 text-gray-600 border border-gray-100'}" th:classappend="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'bg-red-50 text-red-700 border border-red-100' : 'bg-gray-50 text-gray-600 border border-gray-100'}"
th:text="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span> th:text="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
</div> </div>
<div class="text-[10px] text-gray-400 mb-2 truncate" th:title="${dot.equipement.description}" th:text="${dot.equipement.description != null and !dot.equipement.description.isEmpty() ? dot.equipement.description : 'Pas de description'}">Description</div> <div class="text-[10px] text-gray-400 mb-2 truncate" th:title="${dot.equipement.description}" th:text="${dot.equipement.description != null and !dot.equipement.description.isEmpty() ? dot.equipement.description : 'Pas de description'}">Description</div>
<!-- Special dynamic label for equipment style --> <!-- Special dynamic label for equipment style -->
<div class="mb-2" th:if="${dot.equipement.nom == 'Maillot' or dot.equipement.nom == 'Short' or dot.equipement.nom == 'Chaussettes'}"> <div class="mb-2" th:if="${dot.equipement.gestionSexe}">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-50 text-blue-700 border border-blue-100 dotation-style-label" <span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-50 text-blue-700 border border-blue-100 dotation-style-label"
th:data-equipement="${dot.equipement.nom}" th:data-equipement="${dot.equipement.nom}"
th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'} + ' - ' + ${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}"> th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'} + (${dot.equipement.nom == 'Maillot' or dot.equipement.nom == 'Short' or dot.equipement.nom == 'Chaussettes'} ? ' - ' + (${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}) : '')">
Modèle Homme - Joueur
</span>
</div>
<div class="mb-2" th:if="${dot.equipement.nom == 'Survêtement'}">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 border border-indigo-100 dotation-style-label"
th:data-equipement="${dot.equipement.nom}"
th:text="${adherent.sexe == 'FEMININ' ? 'Modèle Femme' : 'Modèle Homme'}">
Modèle Homme Modèle Homme
</span> </span>
</div> </div>
@@ -329,17 +385,43 @@
</div> </div>
<div class="grid grid-cols-2 gap-1.5 mb-1.5"> <div class="grid grid-cols-2 gap-1.5 mb-1.5">
<div> <div th:if="${dot.equipement.taillesDisponibles != null and !dot.equipement.taillesDisponibles.trim().isEmpty()}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label> <label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" placeholder="S, M, L..." <select th:name="'taille_' + ${dot.id}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none"> <option value="">Choisir...</option>
<option th:each="t : ${#strings.arraySplit(dot.equipement.taillesDisponibles, ',')}"
th:value="${#strings.trim(t)}"
th:text="${#strings.trim(t)}"
th:selected="${dot.taille == #strings.trim(t)}"></option>
</select>
</div> </div>
<div th:if="${(dot.equipement.taillesDisponibles == null or dot.equipement.taillesDisponibles.trim().isEmpty()) and (dot.taille != null and !dot.taille.isEmpty())}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 outline-none bg-gray-50 text-gray-500" readonly>
</div>
<div th:if="${dot.equipement.couleursDisponibles != null and !dot.equipement.couleursDisponibles.trim().isEmpty()}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Couleur</label>
<select th:name="'couleur_' + ${dot.id}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
<option value="">Choisir...</option>
<option th:each="c : ${#strings.arraySplit(dot.equipement.couleursDisponibles, ',')}"
th:value="${#strings.trim(c)}"
th:text="${#strings.trim(c)}"
th:selected="${dot.couleur == #strings.trim(c)}"></option>
</select>
</div>
<div th:if="${(dot.equipement.couleursDisponibles == null or dot.equipement.couleursDisponibles.trim().isEmpty()) and (dot.couleur != null and !dot.couleur.isEmpty())}">
<label class="block text-[9px] uppercase font-bold text-gray-400">Couleur</label>
<input type="text" th:name="'couleur_' + ${dot.id}" th:value="${dot.couleur}" class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 outline-none bg-gray-50 text-gray-500" readonly>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 mb-1.5">
<div> <div>
<label class="block text-[9px] uppercase font-bold text-gray-400">Numéro</label> <label class="block text-[9px] uppercase font-bold text-gray-400">Numéro</label>
<input type="text" th:name="'numero_' + ${dot.id}" th:value="${dot.numero}" placeholder="Ex: 10" <input type="text" th:name="'numero_' + ${dot.id}" th:value="${dot.numero}" placeholder="Ex: 10"
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none"> class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
</div> </div>
</div>
<div> <div>
<label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label> <label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label>
<input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur" <input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur"
@@ -379,6 +461,7 @@
th:data-nom="${cat.nom}" th:data-nom="${cat.nom}"
th:data-annee-min="${cat.anneeMin}" th:data-annee-min="${cat.anneeMin}"
th:data-annee-max="${cat.anneeMax}" th:data-annee-max="${cat.anneeMax}"
th:data-sexe="${cat.sexe}"
th:data-tarif-base="${cat.tarifBase}" th:data-tarif-base="${cat.tarifBase}"
th:data-tarif-exterieur="${cat.tarifExterieur}" th:data-tarif-exterieur="${cat.tarifExterieur}"
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option> th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
@@ -457,11 +540,19 @@
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label> <label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label>
<select name="modePaiementId" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <select id="addPaiementMode" name="modePaiementId" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" onchange="toggleChequeVisibility(this, 'addChequeBlock', 'addNumeroCheque')">
<option value="">Sélectionnez un mode...</option> <option value="">Sélectionnez un mode...</option>
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option> <option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:data-nom="${#strings.toLowerCase(mode.nom)}" th:text="${mode.nom}"></option>
</select> </select>
</div> </div>
<div id="addChequeBlock" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro du chèque <span class="text-red-500">*</span></label>
<input type="text" id="addNumeroCheque" name="numeroCheque" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Commentaire (facultatif)</label>
<textarea id="addCommentaire" name="commentaire" rows="2" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2"> <div class="items-center px-4 py-3 flex justify-end space-x-2">
<button type="button" onclick="closePaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button> <button type="button" onclick="closePaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700">Payer</button> <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700">Payer</button>
@@ -489,11 +580,19 @@
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label> <label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label>
<select id="editPaiementMode" name="modePaiementId" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <select id="editPaiementMode" name="modePaiementId" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" onchange="toggleChequeVisibility(this, 'editChequeBlock', 'editNumeroCheque')">
<option value="">Sélectionnez un mode...</option> <option value="">Sélectionnez un mode...</option>
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option> <option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:data-nom="${#strings.toLowerCase(mode.nom)}" th:text="${mode.nom}"></option>
</select> </select>
</div> </div>
<div id="editChequeBlock" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro du chèque <span class="text-red-500">*</span></label>
<input type="text" id="editNumeroCheque" name="numeroCheque" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Commentaire (facultatif)</label>
<textarea id="editCommentaire" name="commentaire" rows="2" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
</div>
<div class="items-center px-4 py-3 flex justify-end space-x-2"> <div class="items-center px-4 py-3 flex justify-end space-x-2">
<button type="button" onclick="closeEditPaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button> <button type="button" onclick="closeEditPaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button> <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
@@ -538,28 +637,55 @@
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 et le sexe
const dateNaissanceStr = document.getElementById('dateNaissance').value; const dateNaissanceStr = document.getElementById('dateNaissance').value;
const adherentSexe = document.getElementById('sexe') ? document.getElementById('sexe').value : 'MASCULIN';
let foundCat = false; let foundCat = false;
let fallbackCatId = '';
let preSelectedCatId = ''; let preSelectedCatId = '';
if (dateNaissanceStr) { if (dateNaissanceStr) {
const birthYear = new Date(dateNaissanceStr).getFullYear(); const birthYear = new Date(dateNaissanceStr).getFullYear();
Array.from(catSelect.options).forEach(opt => { Array.from(catSelect.options).forEach(opt => {
if (!opt.value) return;
const min = parseInt(opt.getAttribute('data-annee-min')); const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max')); const max = parseInt(opt.getAttribute('data-annee-max'));
const catSexe = opt.getAttribute('data-sexe') || 'MASCULIN';
if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) { if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) {
catSelect.value = opt.value; if (catSexe === adherentSexe) {
preSelectedCatId = opt.value; catSelect.value = opt.value;
foundCat = true; preSelectedCatId = opt.value;
foundCat = true;
}
if (catSexe === 'MASCULIN') {
fallbackCatId = opt.value;
}
} }
}); });
} }
if (!foundCat && fallbackCatId) {
catSelect.value = fallbackCatId;
preSelectedCatId = fallbackCatId;
foundCat = true;
}
if (!foundCat) { if (!foundCat) {
catSelect.value = ''; catSelect.value = '';
} }
@@ -589,17 +715,35 @@
document.getElementById('paiementModal').classList.add('hidden'); document.getElementById('paiementModal').classList.add('hidden');
} }
function openEditPaiementModal(paiementId, montant, date, modePaiementId, licenceId, maxAmount) { function openEditPaiementModal(paiementId, montant, date, modePaiementId, licenceId, maxAmount, numeroCheque, commentaire) {
document.getElementById('editPaiementModal').classList.remove('hidden'); document.getElementById('editPaiementModal').classList.remove('hidden');
document.getElementById('editPaiementForm').action = '/paiements/' + paiementId + '/update'; document.getElementById('editPaiementForm').action = '/paiements/' + paiementId + '/update';
document.getElementById('editPaiementMontant').value = montant; document.getElementById('editPaiementMontant').value = montant;
document.getElementById('editPaiementMontant').max = maxAmount; document.getElementById('editPaiementMontant').max = maxAmount;
document.getElementById('editPaiementDate').value = date; document.getElementById('editPaiementDate').value = date;
document.getElementById('editPaiementMode').value = modePaiementId; document.getElementById('editPaiementMode').value = modePaiementId;
document.getElementById('editNumeroCheque').value = numeroCheque || '';
document.getElementById('editCommentaire').value = commentaire || '';
toggleChequeVisibility(document.getElementById('editPaiementMode'), 'editChequeBlock', 'editNumeroCheque');
} }
function closeEditPaiementModal() { function closeEditPaiementModal() {
document.getElementById('editPaiementModal').classList.add('hidden'); document.getElementById('editPaiementModal').classList.add('hidden');
} }
function toggleChequeVisibility(selectElement, blockId, inputId) {
const block = document.getElementById(blockId);
const input = document.getElementById(inputId);
const selectedOption = selectElement.options[selectElement.selectedIndex];
if (selectedOption && selectedOption.getAttribute('data-nom') && selectedOption.getAttribute('data-nom').includes('chèque')) {
block.classList.remove('hidden');
input.required = true;
} else {
block.classList.add('hidden');
input.required = false;
input.value = '';
}
}
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const dateInput = document.getElementById('dateNaissance'); const dateInput = document.getElementById('dateNaissance');
const repBlock = document.getElementById('representantBlock'); const repBlock = document.getElementById('representantBlock');
@@ -641,11 +785,33 @@
} }
} }
const ancienClubInput = document.getElementById('ancienClub');
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
if (ancienClubInput) {
ancienClubInput.addEventListener('input', function() {
if (this.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden-block');
ancienneCategorieSelect.required = true;
} else {
ancienneCategorieBlock.classList.add('hidden-block');
ancienneCategorieSelect.required = false;
}
});
if (ancienClubInput.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden-block');
ancienneCategorieSelect.required = true;
}
}
function updateLicencesCategoryAndPrices() { function updateLicencesCategoryAndPrices() {
if (!dateInput.value) return; if (!dateInput.value) return;
const dob = new Date(dateInput.value); const dob = new Date(dateInput.value);
const birthYear = dob.getFullYear(); const birthYear = dob.getFullYear();
const isResident = residentCheckbox ? residentCheckbox.checked : true; const isResident = residentCheckbox ? residentCheckbox.checked : true;
const adherentSexe = document.getElementById('sexe') ? document.getElementById('sexe').value : 'MASCULIN';
const categories = []; const categories = [];
const catSelect = document.getElementById('categorieSelect'); const catSelect = document.getElementById('categorieSelect');
@@ -655,17 +821,21 @@
const nom = opt.getAttribute('data-nom'); const nom = opt.getAttribute('data-nom');
const min = parseInt(opt.getAttribute('data-annee-min')); const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max')); const max = parseInt(opt.getAttribute('data-annee-max'));
const sexe = opt.getAttribute('data-sexe') || 'MASCULIN';
const tarifBase = parseFloat(opt.getAttribute('data-tarif-base') || '0'); const tarifBase = parseFloat(opt.getAttribute('data-tarif-base') || '0');
const tarifExterieur = parseFloat(opt.getAttribute('data-tarif-exterieur') || '0'); const tarifExterieur = parseFloat(opt.getAttribute('data-tarif-exterieur') || '0');
if (id) { if (id) {
categories.push({ id, nom, min, max, tarifBase, tarifExterieur }); categories.push({ id, nom, min, max, sexe, tarifBase, tarifExterieur });
} }
}); });
} }
if (categories.length === 0) return; if (categories.length === 0) return;
const matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max); let matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max && c.sexe === adherentSexe);
if (!matchingCat) {
matchingCat = categories.find(c => birthYear >= c.min && birthYear <= c.max && c.sexe === 'MASCULIN');
}
if (!matchingCat) return; if (!matchingCat) return;
document.querySelectorAll('.licence-row').forEach(row => { document.querySelectorAll('.licence-row').forEach(row => {
@@ -813,7 +983,10 @@
} }
if (sexeSelect) { if (sexeSelect) {
sexeSelect.addEventListener('change', updateEquipmentStyleLabels); sexeSelect.addEventListener('change', function() {
updateEquipmentStyleLabels();
updateLicencesCategoryAndPrices();
});
} }
if (typeMaillotSelect) { if (typeMaillotSelect) {
typeMaillotSelect.addEventListener('change', updateEquipmentStyleLabels); typeMaillotSelect.addEventListener('change', updateEquipmentStyleLabels);
@@ -175,7 +175,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${currentPage - 1}}|" th:attr="hx-vals=|{&quot;page&quot;: ${currentPage - 1}}|"
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors"> class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Précédent Précédent
</button> </button>
@@ -190,7 +190,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${pageNum}}|" th:attr="hx-vals=|{&quot;page&quot;: ${pageNum}}|"
th:text="${pageNum + 1}" th:text="${pageNum + 1}"
class="px-3 py-1 rounded transition-colors" class="px-3 py-1 rounded transition-colors"
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}"> th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
@@ -205,7 +205,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${currentPage + 1}}|" th:attr="hx-vals=|{&quot;page&quot;: ${currentPage + 1}}|"
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors"> class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Suivant Suivant
</button> </button>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Utilisateurs Connectés - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<!-- Header -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
<h2 class="text-lg font-semibold text-gray-800">Utilisateurs Connectés</h2>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6">
<div class="mb-6 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-900">
Sessions Actives (<span th:text="${activeCount}">0</span>)
</h3>
<button hx-get="/admin/sessions" hx-target="body" hx-swap="outerHTML" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
Rafraîchir
</button>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
<th class="py-3 px-6 font-medium text-left">Nom d'utilisateur</th>
<th class="py-3 px-6 font-medium text-left">Statut</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr th:if="${#lists.isEmpty(activeUsers)}">
<td colspan="2" class="py-8 text-center text-gray-500">Aucun utilisateur connecté pour le moment.</td>
</tr>
<tr th:each="username : ${activeUsers}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900 flex items-center">
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold mr-3">
<span th:text="${#strings.substring(username, 0, 1).toUpperCase()}">U</span>
</div>
<span th:text="${username}">username</span>
</td>
<td class="py-4 px-6">
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded-full">En ligne</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Logs Paiements - AS Talange</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>
</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 px-6">
<h2 class="text-lg font-semibold text-gray-800">Audit Logs des Paiements</h2>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6">
<!-- Filtres -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-5 mb-6">
<form th:action="@{/admin/logs/paiements}" method="get" class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Action</label>
<select name="action" 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="">Toutes</option>
<option value="CREATE" th:selected="${paramAction == 'CREATE'}">Création</option>
<option value="UPDATE" th:selected="${paramAction == 'UPDATE'}">Modification</option>
<option value="DELETE" th:selected="${paramAction == 'DELETE'}">Suppression</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Agent / Utilisateur</label>
<input type="text" name="utilisateur" th:value="${paramUtilisateur}" placeholder="Ex: admin" 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">Adhérent concerné</label>
<input type="text" name="adherent" th:value="${paramAdherent}" placeholder="Nom ou prénom" 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="flex space-x-2">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors w-full">
Filtrer
</button>
<a href="/admin/logs/paiements" class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center px-3">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</a>
</div>
</form>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse min-w-[800px]">
<thead>
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
<th class="py-3 px-6 font-medium">Date</th>
<th class="py-3 px-6 font-medium">Action</th>
<th class="py-3 px-6 font-medium">Utilisateur</th>
<th class="py-3 px-6 font-medium">Adhérent</th>
<th class="py-3 px-6 font-medium">Détails</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 text-sm">
<tr th:if="${#lists.isEmpty(logs)}">
<td colspan="5" class="py-4 px-6 text-center text-gray-500">Aucun log enregistré.</td>
</tr>
<tr th:each="log : ${logs}" class="hover:bg-gray-50 transition-colors">
<td class="py-3 px-6 text-gray-600 whitespace-nowrap" th:text="${#temporals.format(log.dateAction, 'dd/MM/yyyy HH:mm:ss')}">01/01/2026 10:00:00</td>
<td class="py-3 px-6">
<span class="px-2 py-1 text-xs font-semibold rounded-full"
th:classappend="${log.action == 'CREATE' ? 'bg-green-100 text-green-800' : (log.action == 'UPDATE' ? 'bg-blue-100 text-blue-800' : 'bg-red-100 text-red-800')}"
th:text="${log.action}">CREATE</span>
</td>
<td class="py-3 px-6 font-medium text-gray-900" th:text="${log.utilisateur}">Jean Dupont</td>
<td class="py-3 px-6 text-gray-600" th:text="${log.adherentNomComplet}">John Doe</td>
<td class="py-3 px-6 text-gray-500 text-xs" th:text="${log.details}">Détails du changement</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</body>
</html>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Changement de mot de passe - AS Talange</title> <title>Changement de mot de passe - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -1,46 +1,58 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head> <head>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
</head> </head>
<body> <body>
<aside th:fragment="sidebar" class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <aside th:fragment="sidebar" class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
<div class="h-16 flex items-center px-6 border-b border-gray-200"> <div class="h-16 flex items-center px-6 border-b border-gray-200">
<img src="/images/logo.png" alt="Logo AS Talange" class="h-8 w-8 mr-3 object-contain">
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1> <h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
</div> </div>
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1"> <nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
<!-- Menu Principal -->
<div class="pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Gestion</div>
<a href="/" th:classappend="${requestURI == '/' ? '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">Tableau de bord</a> <a href="/" th:classappend="${requestURI == '/' ? '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">Tableau de bord</a>
<a href="/adherents" th:classappend="${requestURI != null and requestURI.startsWith('/adherents') ? '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">Adhérents</a> <a href="/adherents" th:classappend="${requestURI != null and requestURI.startsWith('/adherents') ? '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">Adhérents</a>
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_AGENT_SAISIE')" href="/admin/pre-inscriptions" th:classappend="${requestURI != null and requestURI.startsWith('/admin/pre-inscriptions') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="flex items-center justify-between px-3 py-2 rounded-lg transition-colors">
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_AGENT_SAISIE')" href="/admin/pre-inscriptions" th:classappend="${requestURI != null and requestURI.startsWith('/admin/pre-inscriptions') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="flex items-center px-3 py-2 rounded-lg transition-colors">
<span>Pré-inscriptions</span> <span>Pré-inscriptions</span>
<span hx-get="/admin/pre-inscriptions/count" hx-trigger="load, every 30s, refreshBadge from:body" hx-swap="outerHTML"></span> <span hx-get="/admin/pre-inscriptions/count" hx-trigger="load, every 30s, refreshBadge from:body" hx-swap="outerHTML"></span>
</a> </a>
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_TRESORERIE')" href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/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">Paiements</a> <a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_TRESORERIE')" href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/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">Paiements</a>
<th:block sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_AGENT_SAISIE')"> <th:block sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_AGENT_SAISIE')">
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Planning</div> <a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? '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">Planning</a>
<a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? '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">Entraînements</a>
</th:block> </th:block>
<th:block sec:authorize="hasRole('ROLE_ADMIN')"> <th:block sec:authorize="hasRole('ROLE_ADMIN')">
<!-- Recherches & Exports -->
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Recherches & Exports</div>
<a href="/admin/licences/recherche" th:classappend="${requestURI != null and requestURI.startsWith('/admin/licences/recherche') ? '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">Recherche Licences</a>
<a href="/admin/equipements/recherche" th:classappend="${requestURI != null and requestURI.startsWith('/admin/equipements/recherche') ? '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">Recherche Équipements</a>
<!-- Paramétrage -->
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div> <div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? '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">Saisons</a> <a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? '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">Saisons</a>
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? '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">Catégories</a> <a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? '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">Catégories</a>
<a href="/equipes" th:classappend="${requestURI != null and requestURI.startsWith('/equipes') ? '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">Équipes</a> <a href="/equipes" th:classappend="${requestURI != null and requestURI.startsWith('/equipes') ? '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">Équipes</a>
<a href="/educateurs" th:classappend="${requestURI != null and requestURI.startsWith('/educateurs') ? '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">Éducateurs</a> <a href="/educateurs" th:classappend="${requestURI != null and requestURI.startsWith('/educateurs') ? '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">Éducateurs</a>
<a href="/equipements" th:classappend="${requestURI != null and requestURI.equals('/equipements') ? '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">Équipements</a>
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? '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">Modes de Paiement</a> <a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? '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">Modes de Paiement</a>
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? '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">Équipements</a>
<!-- Administration -->
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div> <div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div>
<a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a> <a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a>
<a href="/admin/logs/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/admin/logs/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Logs Paiements</a>
<a href="/admin/sessions" th:classappend="${requestURI != null and requestURI.startsWith('/admin/sessions') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs Connectés</a>
</th:block> </th:block>
</nav> </nav>
<div class="p-4 border-t border-gray-200"> <div class="p-4 border-t border-gray-200">
<div class="text-sm font-medium text-gray-900 mb-1" th:text="${username != null ? username : 'Admin'}">Admin</div> <div class="text-sm font-medium text-gray-900 mb-1" sec:authentication="name">Admin</div>
<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>
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Tableau de bord - AS Talange</title> <title>Tableau de bord - AS Talange</title>
@@ -18,13 +18,23 @@
<!-- Main Content --> <!-- Main Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <main class="flex-1 flex flex-col h-screen overflow-hidden">
<!-- Header --> <!-- Header -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center 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>
</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 -->
@@ -51,7 +61,7 @@
</div> </div>
<!-- Total Encaissé --> <!-- Total Encaissé -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4"> <div sec:authorize="!hasRole('ROLE_AGENT_SAISIE')" class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
<div class="p-3 bg-purple-50 text-purple-600 rounded-lg"> <div class="p-3 bg-purple-50 text-purple-600 rounded-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div> </div>
@@ -62,7 +72,7 @@
</div> </div>
<!-- Reste à Recouvrer --> <!-- Reste à Recouvrer -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4"> <div sec:authorize="!hasRole('ROLE_AGENT_SAISIE')" class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
<div class="p-3 bg-red-50 text-red-600 rounded-lg"> <div class="p-3 bg-red-50 text-red-600 rounded-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"></path></svg> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"></path></svg>
</div> </div>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Connexion - AS Talange</title> <title>Connexion - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -13,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>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Catégorie - AS Talange</title> <title>Catégorie - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -21,9 +22,18 @@
<form th:action="@{/categories}" th:object="${categorie}" method="post" class="space-y-6"> <form th:action="@{/categories}" th:object="${categorie}" method="post" class="space-y-6">
<input type="hidden" th:field="*{id}" /> <input type="hidden" th:field="*{id}" />
<div> <div class="grid grid-cols-2 gap-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label> <div>
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label>
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
<select th:field="*{sexe}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="MASCULIN">Masculine</option>
<option value="FEMININ">Féminine</option>
</select>
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -52,30 +62,95 @@
<div class="pt-4 border-t border-gray-100"> <div class="pt-4 border-t border-gray-100">
<label class="block text-sm font-medium text-gray-700 mb-2">Équipements liés à la catégorie</label> <label class="block text-sm font-medium text-gray-700 mb-2">Équipements liés à la catégorie</label>
<div class="space-y-3 max-h-64 overflow-y-auto">
<th:block th:each="equi : ${allEquipements}"> <div class="flex space-x-2 mb-4">
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100"> <select id="equipementToAdd" class="flex-1 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<div class="w-1/3"> <option value="">Sélectionner un équipement à ajouter...</option>
<th:block th:each="equi : ${allEquipements}">
<option th:value="${equi.id}" th:text="${equi.nom}"></option>
</th:block>
</select>
<button type="button" onclick="addEquipement()" class="px-4 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">Ajouter</button>
</div>
<div id="equipementsContainer" class="space-y-3 max-h-64 overflow-y-auto">
<th:block th:each="equi : ${allEquipements}" th:if="${categorie.hasEquipement(equi.id)}">
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row" th:id="'eq-row-' + ${equi.id}">
<div class="w-1/4">
<span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span> <span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span>
</div> </div>
<div class="w-1/3"> <div class="w-1/3">
<select th:name="'etatEquipement_' + ${equi.id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <select th:name="'etatEquipement_' + ${equi.id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="NON_LIE" th:selected="${!categorie.hasEquipement(equi.id)}">Non lié</option> <option value="OBLIGATOIRE" th:selected="${categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option>
<option value="OBLIGATOIRE" th:selected="${categorie.hasEquipement(equi.id) and categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option> <option value="OPTIONNEL" th:selected="${!categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
<option value="OPTIONNEL" th:selected="${categorie.hasEquipement(equi.id) and !categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
</select> </select>
</div> </div>
<div class="w-1/3 flex items-center space-x-2"> <div class="w-1/4 flex items-center space-x-2">
<label class="text-xs text-gray-500">Prix (€)</label> <label class="text-xs text-gray-500">Prix (€)</label>
<input type="number" step="0.01" th:name="'prixEquipement_' + ${equi.id}" <input type="number" step="0.01" th:name="'prixEquipement_' + ${equi.id}"
th:value="${categorie.getEquipementPrix(equi.id)}" th:value="${categorie.getEquipementPrix(equi.id)}"
class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div> </div>
<div class="w-auto">
<button type="button" th:onclick="'removeEquipement(' + ${equi.id} + ')'" class="text-red-600 hover:text-red-800 text-sm font-medium">Retirer</button>
</div>
</div> </div>
</th:block> </th:block>
</div> </div>
</div> </div>
<script>
function addEquipement() {
const select = document.getElementById('equipementToAdd');
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption.value) return;
const id = selectedOption.value;
const nom = selectedOption.text;
if (document.getElementById('eq-row-' + id)) {
alert("Cet équipement est déjà lié à la catégorie.");
return;
}
const container = document.getElementById('equipementsContainer');
const row = document.createElement('div');
row.className = 'flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row';
row.id = 'eq-row-' + id;
row.innerHTML = `
<div class="w-1/4">
<span class="block text-sm font-medium text-gray-900">${nom}</span>
</div>
<div class="w-1/3">
<select name="etatEquipement_${id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="OBLIGATOIRE">Obligatoire (inclus)</option>
<option value="OPTIONNEL">Optionnel (payant)</option>
</select>
</div>
<div class="w-1/4 flex items-center space-x-2">
<label class="text-xs text-gray-500">Prix (€)</label>
<input type="number" step="0.01" name="prixEquipement_${id}" value="0.00" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div class="w-auto">
<button type="button" onclick="removeEquipement(${id})" class="text-red-600 hover:text-red-800 text-sm font-medium">Retirer</button>
</div>
`;
container.appendChild(row);
select.value = '';
}
function removeEquipement(id) {
const row = document.getElementById('eq-row-' + id);
if (row) {
row.remove();
}
}
</script>
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
<a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a> <a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button> <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Paramétrage - Catégories</title> <title>Paramétrage - Catégories</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -19,9 +20,12 @@
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Liste des Catégories</h3> <h3 class="text-xl font-bold text-gray-900">Liste des Catégories</h3>
<a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700"> <div class="flex items-center space-x-4">
+ Nouvelle Catégorie <input type="text" id="searchInput" placeholder="Rechercher une catégorie..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
</a> <a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvelle Catégorie
</a>
</div>
</div> </div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
@@ -42,7 +46,10 @@
<td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td> <td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td>
</tr> </tr>
<tr th:each="cat : ${categories}" class="hover:bg-gray-50 transition-colors"> <tr th:each="cat : ${categories}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900" th:text="${cat.nom}">U15</td> <td class="py-4 px-6">
<div class="font-medium text-gray-900" th:text="${cat.nom}">U15</div>
<div class="text-xs text-gray-500" th:text="${cat.sexe == 'FEMININ' ? 'Féminine' : 'Masculine'}">Masculine</div>
</td>
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMin}">2010</td> <td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMin}">2010</td>
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMax}">2011</td> <td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMax}">2011</td>
<td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td> <td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td>
@@ -60,5 +67,15 @@
</div> </div>
</div> </div>
</main> </main>
<script>
document.getElementById('searchInput').addEventListener('keyup', function() {
let filter = this.value.toLowerCase();
let rows = document.querySelectorAll('tbody tr.hover\\:bg-gray-50');
rows.forEach(row => {
let text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
});
</script>
</body> </body>
</html> </html>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Éducateur - AS Talange</title> <title>Éducateur - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Paramétrage - Éducateurs</title> <title>Paramétrage - Éducateurs</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Équipement - AS Talange</title> <title>Équipement - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -26,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"> <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>
</div> </div>
<div class="pt-4 border-t border-gray-100">
<h3 class="text-sm font-medium text-gray-900 mb-4">Options de déclinaison (facultatif)</h3>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" th:field="*{gestionSexe}" class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<label class="ml-2 block text-sm text-gray-700">Gérer les coupes Homme/Femme</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Type de public (Poste)</label>
<select th:field="*{typePublic}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="TOUS">Tous (Joueurs et Gardiens)</option>
<option value="JOUEUR">Joueurs de champ uniquement</option>
<option value="GARDIEN">Gardiens de but uniquement</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Tailles disponibles</label>
<input type="text" th:field="*{taillesDisponibles}" placeholder="ex: XS, S, M, L, XL ou 5/6 ANS, 7/8 ANS..." class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<p class="text-xs text-gray-500 mt-1">Séparez les tailles par des virgules. Laissez vide si taille unique ou standard.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Couleurs disponibles</label>
<input type="text" th:field="*{couleursDisponibles}" placeholder="ex: Bleu, Rouge, Blanc..." class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<p class="text-xs text-gray-500 mt-1">Séparez les couleurs par des virgules. Laissez vide si couleur unique.</p>
</div>
</div>
</div>
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100"> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
<a th:href="@{/equipements}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a> <a th:href="@{/equipements}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button> <button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Paramétrage - Équipements</title> <title>Paramétrage - Équipements</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -19,12 +20,13 @@
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-gray-900">Liste des Équipements</h3> <h3 class="text-xl font-bold text-gray-900">Liste des Équipements</h3>
<a th:href="@{/equipements/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700"> <div class="flex items-center space-x-4">
+ Nouvel Équipement <input type="text" id="searchInput" placeholder="Rechercher un équipement..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64">
</a> <a th:href="@{/equipements/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvel Équipement
</a>
</div>
</div> </div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-left border-collapse"> <table class="w-full text-left border-collapse">
<thead> <thead>
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200"> <tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
@@ -52,5 +54,15 @@
</div> </div>
</div> </div>
</main> </main>
<script>
document.getElementById('searchInput').addEventListener('keyup', function() {
let filter = this.value.toLowerCase();
let rows = document.querySelectorAll('tbody tr.hover\\:bg-gray-50');
rows.forEach(row => {
let text = row.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
});
});
</script>
</body> </body>
</html> </html>
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8">
<title>Paramétrage - Recherche Équipements</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>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
<h2 class="text-lg font-semibold text-gray-800">Recherche Équipements</h2>
</header>
<div class="flex-1 overflow-auto p-6">
<div class="bg-white p-6 rounded-lg border border-gray-200 shadow-sm mb-6">
<form th:action="@{/admin/equipements/recherche}" method="get" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Équipement</label>
<select name="equipementId" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2 border">
<option value="">Tous les équipements</option>
<option th:each="equip : ${equipements}" th:value="${equip.id}" th:text="${equip.nom}" th:selected="${equip.id == equipementId}"></option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
<select name="categorieId" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2 border">
<option value="">Toutes les catégories</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom}" th:selected="${cat.id == categorieId}"></option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Fourni</label>
<select name="fourni" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2 border">
<option value="">Tous</option>
<option value="true" th:selected="${fourni != null && fourni}">Oui</option>
<option value="false" th:selected="${fourni != null && !fourni}">Non</option>
</select>
</div>
</div>
<div class="flex justify-end space-x-3">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">Rechercher</button>
<a th:href="@{/admin/equipements/recherche/export(equipementId=${equipementId},categorieId=${categorieId},fourni=${fourni})}" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700">Exporter</a>
</div>
</form>
</div>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
<th class="py-3 px-4 font-medium text-left">Licencié</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">É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>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr th:if="${#lists.isEmpty(dotations)}">
<td colspan="6" class="py-8 text-center text-gray-500">Aucun résultat trouvé pour cette recherche.</td>
</tr>
<tr th:each="dotation : ${dotations}" class="hover:bg-gray-50">
<td class="py-3 px-4 font-medium text-gray-900">
<span th:text="${dotation.licence.adherent.nom + ' ' + dotation.licence.adherent.prenom}">Nom Prénom</span>
</td>
<td class="py-3 px-4 text-gray-600 text-xs">
<div th:text="${dotation.licence.adherent.typeMaillot}">Joueur</div>
<div th:text="${dotation.licence.adherent.sexe}">Masculin</div>
</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>
<div th:if="${dotation.numero != null && !dotation.numero.isEmpty()}" th:text="'N: ' + ${dotation.numero}"></div>
</td>
<td class="py-3 px-4 text-center">
<span th:if="${dotation.fourni}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">Oui</span>
<span th:unless="${dotation.fourni}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Non</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Paramétrage - Équipes</title> <title>Paramétrage - Équipes</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Recherche Licences - 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">
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
<h2 class="text-lg font-semibold text-gray-800">Recherche de Licences</h2>
</header>
<div class="flex-1 overflow-auto p-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
<form th:action="@{/admin/licences/recherche}" method="get" class="flex flex-wrap items-end gap-4">
<div class="w-48">
<label class="block text-sm font-medium text-gray-700 mb-1">Nom</label>
<input type="text" name="nom" th:value="${nomFilter}" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="w-48">
<label class="block text-sm font-medium text-gray-700 mb-1">Prénom</label>
<input type="text" name="prenom" th:value="${prenomFilter}" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="w-48">
<label class="block text-sm font-medium text-gray-700 mb-1">N° Licence</label>
<input type="text" name="numeroLicence" th:value="${numeroLicenceFilter}" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="w-64">
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="text" name="email" th:value="${emailFilter}" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="w-48">
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
<select name="categorieId" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Toutes les catégories</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom}" th:selected="${cat.id == categorieId}"></option>
</select>
</div>
<div class="w-48">
<label class="block text-sm font-medium text-gray-700 mb-1">Type de Demande</label>
<select name="typeDemande" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Tous les types</option>
<option value="Nouvelle demande" th:selected="${typeDemandeFilter == 'Nouvelle demande'}">Nouvelle Demande</option>
<option value="Renouvellement" th:selected="${typeDemandeFilter == 'Renouvellement'}">Renouvellement</option>
<option value="Mutation" th:selected="${typeDemandeFilter == 'Mutation'}">Mutation</option>
</select>
</div>
<div class="flex space-x-2">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">Rechercher</button>
<a th:href="@{/admin/licences/recherche}" class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-200">Réinitialiser</a>
<a th:href="@{/admin/licences/recherche/export(nom=${nomFilter},prenom=${prenomFilter},categorieId=${categorieId},numeroLicence=${numeroLicenceFilter},email=${emailFilter},typeDemande=${typeDemandeFilter})}"
class="bg-indigo-50 text-indigo-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-100 flex items-center">
Exporter CSV
</a>
</div>
</form>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse min-w-[900px]">
<thead>
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
<th class="py-3 px-4 font-medium text-left">Nom Prénom</th>
<th class="py-3 px-4 font-medium text-left">Catégorie</th>
<th class="py-3 px-4 font-medium text-left">N° Licence</th>
<th class="py-3 px-4 font-medium text-left">Type de Demande</th>
<th class="py-3 px-4 font-medium text-left">Email</th>
<th class="py-3 px-4 font-medium text-left">État</th>
<th class="py-3 px-4 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr th:if="${#lists.isEmpty(licences)}">
<td colspan="7" class="py-8 text-center text-gray-500">Aucun résultat trouvé pour cette recherche.</td>
</tr>
<tr th:each="licence : ${licences}" class="hover:bg-gray-50">
<td class="py-3 px-4 font-medium text-gray-900">
<span th:text="${licence.adherent.nom + ' ' + licence.adherent.prenom}">Nom Prénom</span>
</td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.categorie != null ? licence.categorie.nom : '-'}">Catégorie</td>
<td class="py-3 px-4 text-gray-600 font-mono" th:text="${licence.numeroLicence != null ? licence.numeroLicence : '-'}"></td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.typeDemande != null ? licence.typeDemande : '-'}">Type</td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.adherent.email != null ? licence.adherent.email : '-'}">Email</td>
<td class="py-3 px-4 text-gray-600" th:text="${licence.etat != null ? licence.etat : '-'}">Etat</td>
<td class="py-3 px-4 text-right">
<a th:href="@{/adherents/{id}/edit(id=${licence.adherent.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium">Voir Profil</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</body>
</html>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Mode de Paiement - AS Talange</title> <title>Mode de Paiement - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Paramétrage - Modes de Paiement</title> <title>Paramétrage - Modes de Paiement</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Nouvelle Saison - AS Talange</title> <title>Nouvelle Saison - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Paramétrage - Saisons</title> <title>Paramétrage - Saisons</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Créneau d'Entraînement - AS Talange</title> <title>Créneau d'Entraînement - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Planning des Entraînements - AS Talange</title> <title>Planning des Entraînements - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -2,6 +2,7 @@
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <head>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>AS Talange - Pré-inscription</title> <title>AS Talange - Pré-inscription</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -19,20 +20,25 @@
<img src="/images/logo.png" alt="AS Talange" class="h-32 w-auto mx-auto mb-6 object-contain drop-shadow-sm" <img src="/images/logo.png" alt="AS Talange" class="h-32 w-auto mx-auto mb-6 object-contain drop-shadow-sm"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Inscription au club</h1> <h1 class="text-2xl font-bold text-gray-900 mb-2">Inscription au club</h1>
<p th:unless="${inscriptionsFermees}" class="text-gray-600 mb-8">Veuillez valider le captcha pour accéder au formulaire sécurisé.</p> <p th:unless="${inscriptionsFermees}" class="text-gray-600 mb-8">Veuillez valider le captcha pour accéder au
formulaire sécurisé.</p>
<div th:if="${error != null and (inscriptionsFermees == null or !inscriptionsFermees)}"
class="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm" th:text="${error}"></div>
<div th:if="${error != null and (inscriptionsFermees == null or !inscriptionsFermees)}" class="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm" th:text="${error}"></div>
<div th:if="${inscriptionsFermees}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm font-medium"> <div th:if="${inscriptionsFermees}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm font-medium">
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" class="flex flex-col items-center"> <p th:unless="${inscriptionsFermees}" class="text-gray-600 mb-8 leading-relaxed">
<!-- Remplacer par la vraie clé de site Cloudflare Turnstile --> Afin de sécuriser le processus et d'éviter les soumissions automatisées (robots),
<div class="cf-turnstile mb-6" data-sitekey="1x00000000000000000000AA"></div> nous vous demandons de bien vouloir valider le test de sécurité ci-dessous avant de continuer.
</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>
@@ -1,11 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <head>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<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">
@@ -13,9 +24,70 @@
<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" 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 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>
@@ -27,6 +99,20 @@
</div> </div>
</div> </div>
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700">Sexe <span class="text-red-500">*</span></label>
<div class="flex items-center space-x-6">
<label class="flex items-center">
<input type="radio" th:field="*{sexe}" value="MASCULIN" required class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">Masculin</span>
</label>
<label class="flex items-center">
<input type="radio" th:field="*{sexe}" value="FEMININ" required class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">Féminin</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label> <label class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
@@ -55,20 +141,104 @@
<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> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Email <span class="text-red-500">*</span></label> <div>
<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"> <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>
<!-- Removed Adresse --> <div class="flex items-start pt-2">
<div class="flex items-center h-5">
<input id="consentementRgpd" name="consentementRgpd" type="checkbox" required class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
</div>
<div class="ml-3 text-sm">
<label for="consentementRgpd" class="font-medium text-gray-700">Consentement RGPD <span class="text-red-500">*</span></label>
<p class="text-gray-500 text-xs mt-1">J'accepte que l'AS Talange recueille et traite ces données pour la gestion de mon inscription et la logistique de la saison.</p>
</div>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors mt-4"> <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors mt-4">
Soumettre le dossier Soumettre le dossier
</button> </button>
<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> </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');
}
const ancienClubInput = document.getElementById('ancienClub');
const ancienneCategorieBlock = document.getElementById('ancienneCategorieBlock');
const ancienneCategorieSelect = document.getElementById('ancienneCategorie');
if (ancienClubInput) {
ancienClubInput.addEventListener('input', function() {
if (this.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden');
ancienneCategorieSelect.required = true;
} else {
ancienneCategorieBlock.classList.add('hidden');
ancienneCategorieSelect.required = false;
}
});
// Initial check
if (ancienClubInput.value.trim().length > 0) {
ancienneCategorieBlock.classList.remove('hidden');
ancienneCategorieSelect.required = true;
}
}
document.getElementById('dateNaissance').addEventListener('change', function() { document.getElementById('dateNaissance').addEventListener('change', function() {
const dateInput = this.value; const dateInput = this.value;
const repBlock = document.getElementById('representantBlock'); const repBlock = document.getElementById('representantBlock');
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Lien expiré</title> <title>Lien expiré</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -1,6 +1,8 @@
<!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>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Inscription réussie</title> <title>Inscription réussie</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -15,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>
<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> </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>
@@ -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://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Ajouter un Utilisateur - AS Talange</title> <title>Ajouter un Utilisateur - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head> <head>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Gestion des Utilisateurs - AS Talange</title> <title>Gestion des Utilisateurs - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
+4 -1
View File
@@ -1,4 +1,3 @@
services: services:
db: db:
image: postgres:16 image: postgres:16
@@ -11,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
@@ -22,6 +22,7 @@ services:
- "5050:80" - "5050:80"
depends_on: depends_on:
- db - db
restart: always
app: app:
build: build:
@@ -34,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
+14
View File
@@ -0,0 +1,14 @@
import os
import glob
template_dir = "/home/ucef/.gemini/antigravity/scratch/as-talange/as-talange-web/src/main/resources/templates"
html_files = glob.glob(template_dir + "/**/*.html", recursive=True)
for file in html_files:
with open(file, "r") as f:
content = f.read()
if "htmx.org" not in content and "<head>" in content:
content = content.replace("<head>", "<head>\n <script src=\"https://unpkg.com/htmx.org@1.9.11\"></script>")
with open(file, "w") as f:
f.write(content)
print(f"Fixed {file}")
+1 -1
View File
@@ -13,7 +13,7 @@
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.5-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>as-talange-parent</name> <name>as-talange-parent</name>