Compare commits
76 Commits
499be006c0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 19cee92437 | |||
| 214ec08229 | |||
| 62209ae659 | |||
| fc3373a1b4 | |||
| db3eb3ab9a | |||
| d1f38f11f0 | |||
| acf9d6d301 | |||
| 3066ff0e86 | |||
| 64ad4be7dd | |||
| d30e795325 | |||
| e0388c7884 | |||
| 7cf29f6106 | |||
| bef01e3a0e | |||
| 9cf1d26018 | |||
| 94d672c0f6 | |||
| 8156c8f3d0 | |||
| 7249b45533 | |||
| 7e036c1966 | |||
| 194986f880 | |||
| d834b8d0d3 | |||
| e3abe53eb0 | |||
| 348d6e6ccf | |||
| e670e38343 | |||
| fe3a1ec36c | |||
| 44ae524626 | |||
| 829a69f1a5 | |||
| f8336fb407 | |||
| ab1094d29a | |||
| 3477461fb2 | |||
| bb48f919b0 | |||
| 5f2bf61829 | |||
| 2df3e792f1 | |||
| 8ecdc54028 | |||
| e8baeab0e0 | |||
| d0fde2f2e8 | |||
| 754a0c8dcc | |||
| bbbb3978b9 | |||
| 5e75767f92 | |||
| 2f8c8f0829 | |||
| bf38e01c49 | |||
| 6ea793de87 | |||
| 60257f4823 | |||
| 5c64184c93 | |||
| 2dc39521c4 | |||
| c7d8aaa179 | |||
| 62d1158a8a | |||
| 48c3220013 | |||
| f1bd5d9e08 | |||
| 5737fb6429 | |||
| 96eee4326a | |||
| 14c83be77a | |||
| 08dc39f61a | |||
| 84265660b9 | |||
| da33ce934d | |||
| e867acbb70 | |||
| c4bb9803ed | |||
| e1cd5930fd | |||
| d913579c77 | |||
| 2adbdde121 | |||
| bcf92487ab | |||
| bf4c7b36bd | |||
| 3ed9d0454b | |||
| 569927c644 | |||
| 7d39e561f7 | |||
| 8dfe71ac97 | |||
| 87c65c6ebd | |||
| bb24363500 | |||
| 19139124ca | |||
| 2e8298b3b6 | |||
| bca84be16a | |||
| 1a4ff6fdac | |||
| aba4ed0770 | |||
| e39921f049 | |||
| 52fbd5fcf8 | |||
| b6651fabb4 | |||
| 9531019aae |
@@ -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
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: AS Talange CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Build & Run Unit Tests
|
||||||
|
runs-on: [self-hosted, ubuntu-latest]
|
||||||
|
container:
|
||||||
|
image: maven:3.9.6-eclipse-temurin-21
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
run: |
|
||||||
|
if ! command -v git &> /dev/null; then
|
||||||
|
apt-get update && apt-get install -y git
|
||||||
|
fi
|
||||||
|
SERVER_URL="${{ github.server_url }}"
|
||||||
|
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
||||||
|
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
||||||
|
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||||
|
git checkout ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Run Maven Tests
|
||||||
|
run: mvn clean test
|
||||||
|
|
||||||
|
deploy_test:
|
||||||
|
name: Deploy to Test Environment
|
||||||
|
needs: test
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
runs-on: [self-hosted, ubuntu-latest]
|
||||||
|
container:
|
||||||
|
image: docker:latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
run: |
|
||||||
|
apk add --no-cache git
|
||||||
|
SERVER_URL="${{ github.server_url }}"
|
||||||
|
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
||||||
|
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
||||||
|
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
||||||
|
git checkout ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Deploy with Docker Compose
|
||||||
|
env:
|
||||||
|
DB_USER: ${{ secrets.DB_USER }}
|
||||||
|
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "POSTGRES_USER=${DB_USER:-myuser}" > .env
|
||||||
|
echo "POSTGRES_PASSWORD=${DB_PASSWORD:-mypassword}" >> .env
|
||||||
|
echo "CAPTCHA_SECRET=1x0000000000000000000000000000000AA" >> .env
|
||||||
|
echo "CAPTCHA_SITEKEY=1x00000000000000000000AA" >> .env
|
||||||
|
docker compose down app || true
|
||||||
|
docker compose up -d --build app
|
||||||
|
|
||||||
|
- name: Verify application startup
|
||||||
|
run: |
|
||||||
|
echo "Waiting for application to start..."
|
||||||
|
COUNT=0
|
||||||
|
while [ $COUNT -lt 30 ]; do
|
||||||
|
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
|
||||||
|
echo "Application is up and login page is accessible!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... ($COUNT/30)"
|
||||||
|
sleep 5
|
||||||
|
COUNT=$((COUNT+1))
|
||||||
|
done
|
||||||
|
echo "Application failed to start or login page is not accessible."
|
||||||
|
docker logs astalange_app
|
||||||
|
exit 1
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: AS Talange CI/CD Pipeline
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Build & Run Unit Tests
|
|
||||||
runs-on: self-hosted
|
|
||||||
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 --depth 1 "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
|
||||||
git checkout ${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Run Maven Tests
|
|
||||||
run: mvn clean test
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Build & Run in Docker Container
|
|
||||||
needs: test
|
|
||||||
runs-on: self-hosted
|
|
||||||
container:
|
|
||||||
image: docker:latest
|
|
||||||
steps:
|
|
||||||
- name: Clone repository
|
|
||||||
run: |
|
|
||||||
apk add --no-cache git
|
|
||||||
SERVER_URL="${{ github.server_url }}"
|
|
||||||
SERVER_NO_PROTO="${SERVER_URL#http://}"
|
|
||||||
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
|
|
||||||
git clone --depth 1 "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
|
|
||||||
git checkout ${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Deploy with Docker Compose
|
|
||||||
run: |
|
|
||||||
docker compose down app || true
|
|
||||||
docker compose up -d --build app
|
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>as-talange-parent</artifactId>
|
<artifactId>as-talange-parent</artifactId>
|
||||||
<groupId>com.astalange</groupId>
|
<groupId>com.astalange</groupId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.5-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>as-talange-parent</artifactId>
|
<artifactId>as-talange-parent</artifactId>
|
||||||
<groupId>com.astalange</groupId>
|
<groupId>com.astalange</groupId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.5-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,33 @@ public class Adherent {
|
|||||||
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE)
|
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE)
|
||||||
private LocalDate dateNaissance;
|
private LocalDate dateNaissance;
|
||||||
|
|
||||||
@Column(length = 20)
|
@Column(name = "lieu_naissance_ville", nullable = false, length = 100)
|
||||||
private String telephone;
|
private String lieuNaissanceVille;
|
||||||
|
|
||||||
@Column(length = 100)
|
@Column(name = "lieu_naissance_pays", nullable = false, length = 100)
|
||||||
|
private String lieuNaissancePays;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String nationalite;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(nullable = false, length = 20)
|
||||||
private String adresse;
|
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)
|
||||||
@@ -63,18 +78,33 @@ public class Adherent {
|
|||||||
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; }
|
||||||
|
|
||||||
public String getTelephone() { return telephone; }
|
public String getLieuNaissanceVille() { return lieuNaissanceVille; }
|
||||||
public void setTelephone(String telephone) { this.telephone = telephone; }
|
public void setLieuNaissanceVille(String lieuNaissanceVille) { this.lieuNaissanceVille = lieuNaissanceVille; }
|
||||||
|
|
||||||
|
public String getLieuNaissancePays() { return lieuNaissancePays; }
|
||||||
|
public void setLieuNaissancePays(String lieuNaissancePays) { this.lieuNaissancePays = lieuNaissancePays; }
|
||||||
|
|
||||||
|
public String getNationalite() { return nationalite; }
|
||||||
|
public void setNationalite(String nationalite) { this.nationalite = nationalite; }
|
||||||
|
|
||||||
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 getAdresse() { return adresse; }
|
public String getTelephone() { return telephone; }
|
||||||
public void setAdresse(String adresse) { this.adresse = adresse; }
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,25 @@ package com.astalange.core.entity;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "categorie")
|
@Table(name = "categorie", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = {"nom", "saison_id"})
|
||||||
|
})
|
||||||
public class Categorie {
|
public class Categorie {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(nullable = false, unique = true, 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;
|
||||||
|
|
||||||
@@ -30,6 +37,9 @@ public class Categorie {
|
|||||||
@JoinColumn(name = "saison_id", nullable = false)
|
@JoinColumn(name = "saison_id", nullable = false)
|
||||||
private Saison saison;
|
private Saison saison;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "categorie", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<CategorieEquipement> categorieEquipements = new ArrayList<>();
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -38,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; }
|
||||||
|
|
||||||
@@ -52,4 +65,58 @@ public class Categorie {
|
|||||||
|
|
||||||
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 List<CategorieEquipement> getCategorieEquipements() { return categorieEquipements; }
|
||||||
|
public void setCategorieEquipements(List<CategorieEquipement> categorieEquipements) { this.categorieEquipements = categorieEquipements; }
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public boolean hasEquipement(Long equipementId) {
|
||||||
|
if (categorieEquipements == null) return false;
|
||||||
|
return categorieEquipements.stream().anyMatch(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public boolean isEquipementObligatoire(Long equipementId) {
|
||||||
|
if (categorieEquipements == null) return false;
|
||||||
|
return categorieEquipements.stream().anyMatch(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId) && Boolean.TRUE.equals(ce.getObligatoire()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public BigDecimal getEquipementPrix(Long equipementId) {
|
||||||
|
if (categorieEquipements == null) return BigDecimal.ZERO;
|
||||||
|
return categorieEquipements.stream()
|
||||||
|
.filter(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId))
|
||||||
|
.findFirst()
|
||||||
|
.map(CategorieEquipement::getPrix)
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public BigDecimal getEquipementPrixByNom(String nom) {
|
||||||
|
if (categorieEquipements == null || nom == null) return BigDecimal.ZERO;
|
||||||
|
return categorieEquipements.stream()
|
||||||
|
.filter(ce -> ce.getEquipement() != null && nom.equalsIgnoreCase(ce.getEquipement().getNom()))
|
||||||
|
.findFirst()
|
||||||
|
.map(CategorieEquipement::getPrix)
|
||||||
|
.map(p -> p == null ? BigDecimal.ZERO : p)
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public boolean isU11OrBelow() {
|
||||||
|
if (nom == null) return false;
|
||||||
|
String nomCat = nom.trim().toUpperCase();
|
||||||
|
if (nomCat.startsWith("U")) {
|
||||||
|
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^U(\\d+)").matcher(nomCat);
|
||||||
|
if (m.find()) {
|
||||||
|
try {
|
||||||
|
int age = Integer.parseInt(m.group(1));
|
||||||
|
return age <= 11;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.astalange.core.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "categorie_equipement")
|
||||||
|
public class CategorieEquipement {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "categorie_id", nullable = false)
|
||||||
|
private Categorie categorie;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "equipement_id", nullable = false)
|
||||||
|
private Equipement equipement;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean obligatoire = false;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private java.math.BigDecimal prix = java.math.BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
|
||||||
|
public java.math.BigDecimal getPrix() { return prix; }
|
||||||
|
public void setPrix(java.math.BigDecimal prix) { this.prix = prix; }
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Categorie getCategorie() { return categorie; }
|
||||||
|
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
||||||
|
|
||||||
|
public Equipement getEquipement() { return equipement; }
|
||||||
|
public void setEquipement(Equipement equipement) { this.equipement = equipement; }
|
||||||
|
|
||||||
|
public Boolean getObligatoire() { return obligatoire; }
|
||||||
|
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
||||||
|
}
|
||||||
@@ -52,6 +52,9 @@ public class CreneauEntrainement {
|
|||||||
@Transient
|
@Transient
|
||||||
private Set<String> categoriesEnConflit = new HashSet<>();
|
private Set<String> categoriesEnConflit = new HashSet<>();
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private String initialesEducateurs = "";
|
||||||
|
|
||||||
// Logic for Gantt diagram grid placement
|
// Logic for Gantt diagram grid placement
|
||||||
|
|
||||||
public int getStartColumn() {
|
public int getStartColumn() {
|
||||||
@@ -214,4 +217,12 @@ public class CreneauEntrainement {
|
|||||||
public void setCategoriesEnConflit(Set<String> categoriesEnConflit) {
|
public void setCategoriesEnConflit(Set<String> categoriesEnConflit) {
|
||||||
this.categoriesEnConflit = categoriesEnConflit;
|
this.categoriesEnConflit = categoriesEnConflit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getInitialesEducateurs() {
|
||||||
|
return initialesEducateurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitialesEducateurs(String initialesEducateurs) {
|
||||||
|
this.initialesEducateurs = initialesEducateurs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ public class Educateur {
|
|||||||
@JoinColumn(name = "categorie_id")
|
@JoinColumn(name = "categorie_id")
|
||||||
private Categorie categorie;
|
private Categorie categorie;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "equipe_id")
|
||||||
|
private Equipe equipe;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -45,4 +49,13 @@ public class Educateur {
|
|||||||
|
|
||||||
public Categorie getCategorie() { return categorie; }
|
public Categorie getCategorie() { return categorie; }
|
||||||
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
||||||
|
|
||||||
|
public Equipe getEquipe() { return equipe; }
|
||||||
|
public void setEquipe(Equipe equipe) { this.equipe = equipe; }
|
||||||
|
|
||||||
|
public String getInitiales() {
|
||||||
|
String p = (prenom != null && !prenom.isEmpty()) ? prenom.substring(0, 1).toUpperCase() : "";
|
||||||
|
String n = (nom != null && !nom.isEmpty()) ? nom.substring(0, 1).toUpperCase() : "";
|
||||||
|
return p + n;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +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(nullable = false)
|
@Column(name = "gestion_sexe", nullable = false)
|
||||||
private Boolean obligatoire = false;
|
private boolean gestionSexe = false;
|
||||||
|
|
||||||
|
@Column(name = "type_public", nullable = false, length = 20)
|
||||||
|
private String typePublic = "TOUS"; // JOUEUR, GARDIEN, TOUS
|
||||||
|
|
||||||
|
@Column(name = "tailles_disponibles", length = 500)
|
||||||
|
private String taillesDisponibles;
|
||||||
|
|
||||||
|
@Column(name = "couleurs_disponibles", length = 500)
|
||||||
|
private String couleursDisponibles;
|
||||||
|
|
||||||
@Column(name = "prix_contribution", nullable = false, precision = 10, scale = 2)
|
|
||||||
private BigDecimal prixContribution = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "saison_id", nullable = false)
|
@JoinColumn(name = "saison_id", nullable = false)
|
||||||
@@ -35,14 +45,23 @@ 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 getObligatoire() { return obligatoire; }
|
public boolean isGestionSexe() { return gestionSexe; }
|
||||||
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
public void setGestionSexe(boolean gestionSexe) { this.gestionSexe = gestionSexe; }
|
||||||
|
|
||||||
public BigDecimal getPrixContribution() { return prixContribution; }
|
public String getTypePublic() { return typePublic; }
|
||||||
public void setPrixContribution(BigDecimal prixContribution) { this.prixContribution = prixContribution; }
|
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; }
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ public class Licence {
|
|||||||
@Column(name = "numero_licence", length = 50)
|
@Column(name = "numero_licence", length = 50)
|
||||||
private String numeroLicence;
|
private String numeroLicence;
|
||||||
|
|
||||||
|
@Column(name = "type_demande", length = 50)
|
||||||
|
private String typeDemande;
|
||||||
|
|
||||||
|
@Column(name = "type_licence", length = 50)
|
||||||
|
private String typeLicence;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<Paiement> paiements = new ArrayList<>();
|
private List<Paiement> paiements = new ArrayList<>();
|
||||||
|
|
||||||
@@ -55,7 +61,7 @@ public class Licence {
|
|||||||
if (dotations != null) {
|
if (dotations != null) {
|
||||||
for (Dotation dot : dotations) {
|
for (Dotation dot : dotations) {
|
||||||
if (dot.getChoisi() && dot.getEquipement() != null) {
|
if (dot.getChoisi() && dot.getEquipement() != null) {
|
||||||
BigDecimal price = dot.getEquipement().getPrixContribution();
|
BigDecimal price = categorie.getEquipementPrix(dot.getEquipement().getId());
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
prixTotal = prixTotal.add(price);
|
prixTotal = prixTotal.add(price);
|
||||||
}
|
}
|
||||||
@@ -83,6 +89,16 @@ public class Licence {
|
|||||||
return reste.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : reste;
|
return reste.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : reste;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public BigDecimal getSommePayee() {
|
||||||
|
if (paiements == null || paiements.isEmpty()) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
return paiements.stream()
|
||||||
|
.map(Paiement::getMontant)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
}
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -103,15 +119,21 @@ public class Licence {
|
|||||||
public String getEtat() { return etat; }
|
public String getEtat() { return etat; }
|
||||||
public void setEtat(String etat) { this.etat = etat; }
|
public void setEtat(String etat) { this.etat = etat; }
|
||||||
|
|
||||||
|
public String getNumeroLicence() { return numeroLicence; }
|
||||||
|
public void setNumeroLicence(String numeroLicence) { this.numeroLicence = numeroLicence; }
|
||||||
|
|
||||||
|
public String getTypeDemande() { return typeDemande; }
|
||||||
|
public void setTypeDemande(String typeDemande) { this.typeDemande = typeDemande; }
|
||||||
|
|
||||||
|
public String getTypeLicence() { return typeLicence; }
|
||||||
|
public void setTypeLicence(String typeLicence) { this.typeLicence = typeLicence; }
|
||||||
|
|
||||||
public List<Paiement> getPaiements() { return paiements; }
|
public List<Paiement> getPaiements() { return paiements; }
|
||||||
public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; }
|
public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; }
|
||||||
|
|
||||||
public List<Dotation> getDotations() { return dotations; }
|
public List<Dotation> getDotations() { return dotations; }
|
||||||
public void setDotations(List<Dotation> dotations) { this.dotations = dotations; }
|
public void setDotations(List<Dotation> dotations) { this.dotations = dotations; }
|
||||||
|
|
||||||
public String getNumeroLicence() { return numeroLicence; }
|
|
||||||
public void setNumeroLicence(String numeroLicence) { this.numeroLicence = numeroLicence; }
|
|
||||||
|
|
||||||
public void addPaiement(Paiement paiement) {
|
public void addPaiement(Paiement paiement) {
|
||||||
paiements.add(paiement);
|
paiements.add(paiement);
|
||||||
paiement.setLicence(this);
|
paiement.setLicence(this);
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.astalange.core.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "pre_inscription")
|
||||||
|
public class PreInscription {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String nom;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String prenom;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 10)
|
||||||
|
private String sexe;
|
||||||
|
|
||||||
|
@Column(name = "date_naissance", nullable = false)
|
||||||
|
private LocalDate dateNaissance;
|
||||||
|
|
||||||
|
@Column(name = "lieu_naissance_ville", nullable = false, length = 100)
|
||||||
|
private String lieuNaissanceVille;
|
||||||
|
|
||||||
|
@Column(name = "lieu_naissance_pays", nullable = false, length = 100)
|
||||||
|
private String lieuNaissancePays;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String nationalite;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 150)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String telephone;
|
||||||
|
|
||||||
|
@Column(name = "representant_legal", length = 150)
|
||||||
|
private String representantLegal;
|
||||||
|
|
||||||
|
@Column(name = "statut_traite", nullable = false)
|
||||||
|
private Boolean statutTraite = false;
|
||||||
|
|
||||||
|
@Column(name = "date_creation", nullable = false)
|
||||||
|
private LocalDateTime dateCreation = LocalDateTime.now();
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "saison_id", nullable = false)
|
||||||
|
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
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getNom() { return nom; }
|
||||||
|
public void setNom(String nom) { this.nom = nom; }
|
||||||
|
|
||||||
|
public String getPrenom() { return prenom; }
|
||||||
|
public void setPrenom(String prenom) { this.prenom = prenom; }
|
||||||
|
|
||||||
|
public String getSexe() { return sexe; }
|
||||||
|
public void setSexe(String sexe) { this.sexe = sexe; }
|
||||||
|
|
||||||
|
public LocalDate getDateNaissance() { return dateNaissance; }
|
||||||
|
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
|
||||||
|
|
||||||
|
public String getLieuNaissanceVille() { return lieuNaissanceVille; }
|
||||||
|
public void setLieuNaissanceVille(String lieuNaissanceVille) { this.lieuNaissanceVille = lieuNaissanceVille; }
|
||||||
|
|
||||||
|
public String getLieuNaissancePays() { return lieuNaissancePays; }
|
||||||
|
public void setLieuNaissancePays(String lieuNaissancePays) { this.lieuNaissancePays = lieuNaissancePays; }
|
||||||
|
|
||||||
|
public String getNationalite() { return nationalite; }
|
||||||
|
public void setNationalite(String nationalite) { this.nationalite = nationalite; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getTelephone() { return telephone; }
|
||||||
|
public void setTelephone(String telephone) { this.telephone = telephone; }
|
||||||
|
|
||||||
|
public String getRepresentantLegal() { return representantLegal; }
|
||||||
|
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
|
||||||
|
|
||||||
|
public Boolean getStatutTraite() { return statutTraite; }
|
||||||
|
public void setStatutTraite(Boolean statutTraite) { this.statutTraite = statutTraite; }
|
||||||
|
|
||||||
|
public LocalDateTime getDateCreation() { return dateCreation; }
|
||||||
|
public void setDateCreation(LocalDateTime dateCreation) { this.dateCreation = dateCreation; }
|
||||||
|
|
||||||
|
public Saison getSaison() { return 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; }
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ public class Saison {
|
|||||||
@Column(name = "est_active", nullable = false)
|
@Column(name = "est_active", nullable = false)
|
||||||
private Boolean estActive = false;
|
private Boolean estActive = false;
|
||||||
|
|
||||||
|
@Column(name = "inscriptions_ouvertes", nullable = false)
|
||||||
|
private Boolean inscriptionsOuvertes = false;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
@@ -43,10 +46,20 @@ public class Saison {
|
|||||||
this.estActive = estActive;
|
this.estActive = estActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getInscriptionsOuvertes() {
|
||||||
|
return inscriptionsOuvertes != null ? inscriptionsOuvertes : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInscriptionsOuvertes(Boolean inscriptionsOuvertes) {
|
||||||
|
this.inscriptionsOuvertes = inscriptionsOuvertes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o)
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
Saison saison = (Saison) o;
|
Saison saison = (Saison) o;
|
||||||
return Objects.equals(id, saison.id);
|
return Objects.equals(id, saison.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.astalange.core.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "token_pre_inscription")
|
||||||
|
public class TokenPreInscription {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "valeur_uuid", nullable = false, unique = true, length = 36)
|
||||||
|
private String valeurUuid;
|
||||||
|
|
||||||
|
@Column(name = "date_expiration", nullable = false)
|
||||||
|
private LocalDateTime dateExpiration;
|
||||||
|
|
||||||
|
@Column(name = "est_utilise", nullable = false)
|
||||||
|
private Boolean estUtilise = false;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getValeurUuid() { return valeurUuid; }
|
||||||
|
public void setValeurUuid(String valeurUuid) { this.valeurUuid = valeurUuid; }
|
||||||
|
|
||||||
|
public LocalDateTime getDateExpiration() { return dateExpiration; }
|
||||||
|
public void setDateExpiration(LocalDateTime dateExpiration) { this.dateExpiration = dateExpiration; }
|
||||||
|
|
||||||
|
public Boolean getEstUtilise() { return estUtilise; }
|
||||||
|
public void setEstUtilise(Boolean estUtilise) { this.estUtilise = estUtilise; }
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public boolean isValide() {
|
||||||
|
return !estUtilise && dateExpiration.isAfter(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.astalange.core.repository;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.Educateur;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface EducateurRepository extends JpaRepository<Educateur, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT e FROM Educateur e LEFT JOIN FETCH e.categorie LEFT JOIN FETCH e.equipe")
|
||||||
|
List<Educateur> findAllWithCategorieAndEquipe();
|
||||||
|
}
|
||||||
@@ -4,10 +4,16 @@ import com.astalange.core.entity.Equipe;
|
|||||||
import com.astalange.core.entity.Saison;
|
import com.astalange.core.entity.Saison;
|
||||||
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 org.springframework.data.jpa.repository.Query;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface EquipeRepository extends JpaRepository<Equipe, Long> {
|
public interface EquipeRepository extends JpaRepository<Equipe, Long> {
|
||||||
List<Equipe> findByCategorieId(Long categorieId);
|
List<Equipe> findByCategorieId(Long categorieId);
|
||||||
|
|
||||||
|
@Query("SELECT e FROM Equipe e JOIN FETCH e.categorie WHERE e.saison = :saison")
|
||||||
List<Equipe> findBySaison(Saison saison);
|
List<Equipe> findBySaison(Saison saison);
|
||||||
|
|
||||||
|
@Query("SELECT e FROM Equipe e JOIN FETCH e.categorie")
|
||||||
|
List<Equipe> findAllWithCategorie();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ 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);
|
||||||
|
|
||||||
long countByEtat(String etat);
|
long countByEtat(String etat);
|
||||||
|
|
||||||
@Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c")
|
@Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c")
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.astalange.core.repository;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.PreInscription;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PreInscriptionRepository extends JpaRepository<PreInscription, Long> {
|
||||||
|
List<PreInscription> findByStatutTraiteFalseOrderByDateCreationDesc();
|
||||||
|
long countByStatutTraiteFalse();
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.astalange.core.repository;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.TokenPreInscription;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TokenPreInscriptionRepository extends JpaRepository<TokenPreInscription, Long> {
|
||||||
|
Optional<TokenPreInscription> findByValeurUuid(String valeurUuid);
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package com.astalange.core.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CaptchaValidationService {
|
||||||
|
|
||||||
|
@Value("${captcha.secret:1x0000000000000000000000000000000AA}")
|
||||||
|
private String captchaSecret;
|
||||||
|
|
||||||
|
@Value("${captcha.url:https://challenges.cloudflare.com/turnstile/v0/siteverify}")
|
||||||
|
private String captchaUrl;
|
||||||
|
|
||||||
|
public boolean validateCaptcha(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
|
||||||
|
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
|
||||||
|
map.add("secret", captchaSecret);
|
||||||
|
map.add("response", token);
|
||||||
|
|
||||||
|
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ResponseEntity<Map> response = restTemplate.postForEntity(captchaUrl, request, Map.class);
|
||||||
|
if (response.getBody() != null && Boolean.TRUE.equals(response.getBody().get("success"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.astalange.core.service;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.*;
|
||||||
|
import com.astalange.core.repository.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CategorieService {
|
||||||
|
|
||||||
|
private final CategorieRepository categorieRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
private final EquipementRepository equipementRepository;
|
||||||
|
private final LicenceRepository licenceRepository;
|
||||||
|
|
||||||
|
public CategorieService(CategorieRepository categorieRepository, SaisonRepository saisonRepository, EquipementRepository equipementRepository, LicenceRepository licenceRepository) {
|
||||||
|
this.categorieRepository = categorieRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
|
this.equipementRepository = equipementRepository;
|
||||||
|
this.licenceRepository = licenceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EquipementConfig(boolean obligatoire, java.math.BigDecimal prix) {}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void saveCategorie(Categorie categorie, java.util.Map<Long, EquipementConfig> equipementConfigs) {
|
||||||
|
Saison activeSaison;
|
||||||
|
Categorie existing = null;
|
||||||
|
if (categorie.getId() == null) {
|
||||||
|
activeSaison = saisonRepository.findByEstActiveTrue()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
|
||||||
|
categorie.setSaison(activeSaison);
|
||||||
|
} else {
|
||||||
|
final Long catId = categorie.getId();
|
||||||
|
existing = categorieRepository.findById(catId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + catId));
|
||||||
|
categorie.setSaison(existing.getSaison());
|
||||||
|
activeSaison = existing.getSaison();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CategorieEquipement
|
||||||
|
if (existing != null) {
|
||||||
|
categorie.getCategorieEquipements().clear();
|
||||||
|
categorie.getCategorieEquipements().addAll(existing.getCategorieEquipements());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CategorieEquipement> nouveauxEquipements = new ArrayList<>();
|
||||||
|
if (equipementConfigs != null) {
|
||||||
|
for (java.util.Map.Entry<Long, EquipementConfig> entry : equipementConfigs.entrySet()) {
|
||||||
|
Long eqId = entry.getKey();
|
||||||
|
EquipementConfig config = entry.getValue();
|
||||||
|
Equipement eq = equipementRepository.findById(eqId).orElse(null);
|
||||||
|
if (eq != null) {
|
||||||
|
CategorieEquipement ce = new CategorieEquipement();
|
||||||
|
ce.setCategorie(categorie);
|
||||||
|
ce.setEquipement(eq);
|
||||||
|
ce.setObligatoire(config.obligatoire());
|
||||||
|
ce.setPrix(config.prix() != null ? config.prix() : java.math.BigDecimal.ZERO);
|
||||||
|
nouveauxEquipements.add(ce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categorie.getCategorieEquipements().clear();
|
||||||
|
categorie.getCategorieEquipements().addAll(nouveauxEquipements);
|
||||||
|
|
||||||
|
categorie = categorieRepository.save(categorie);
|
||||||
|
|
||||||
|
// Mettre à jour les adhérents (via les licences de la saison actuelle)
|
||||||
|
List<Licence> licences = licenceRepository.findBySaisonAndCategorie(activeSaison, categorie);
|
||||||
|
for (Licence licence : licences) {
|
||||||
|
updateDotationsForLicence(licence, nouveauxEquipements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> toRemove = new ArrayList<>();
|
||||||
|
|
||||||
|
// Remove dotations for equipements no longer in the category or not matching public
|
||||||
|
for (Dotation d : currentDotations) {
|
||||||
|
boolean found = false;
|
||||||
|
for (CategorieEquipement ce : categorieEquipements) {
|
||||||
|
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
|
||||||
|
String typePublic = ce.getEquipement().getTypePublic();
|
||||||
|
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
|
||||||
|
|
||||||
|
boolean matchPublic = "TOUS".equals(typePublic) ||
|
||||||
|
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
|
||||||
|
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
|
||||||
|
|
||||||
|
if (matchPublic) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
toRemove.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentDotations.removeAll(toRemove);
|
||||||
|
|
||||||
|
// Add missing dotations
|
||||||
|
for (CategorieEquipement ce : categorieEquipements) {
|
||||||
|
String typePublic = ce.getEquipement().getTypePublic();
|
||||||
|
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
|
||||||
|
|
||||||
|
boolean matchPublic = "TOUS".equals(typePublic) ||
|
||||||
|
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
|
||||||
|
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
|
||||||
|
|
||||||
|
if (!matchPublic) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean found = false;
|
||||||
|
for (Dotation d : currentDotations) {
|
||||||
|
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
|
||||||
|
found = true;
|
||||||
|
if (Boolean.TRUE.equals(ce.getObligatoire()) && !Boolean.TRUE.equals(d.getChoisi())) {
|
||||||
|
d.setChoisi(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
Dotation newDotation = new Dotation();
|
||||||
|
newDotation.setLicence(licence);
|
||||||
|
newDotation.setEquipement(ce.getEquipement());
|
||||||
|
boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom());
|
||||||
|
boolean isNouvelle = false;
|
||||||
|
if (licence.getTypeDemande() != null &&
|
||||||
|
(licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) &&
|
||||||
|
licence.getCategorie() != null) {
|
||||||
|
isNouvelle = true;
|
||||||
|
}
|
||||||
|
newDotation.setChoisi(ce.getObligatoire() || (isMaillot && isNouvelle)); // Checked if obligatoire or if it's a new registration
|
||||||
|
newDotation.setFourni(false);
|
||||||
|
newDotation.setTaille("");
|
||||||
|
newDotation.setNumero("");
|
||||||
|
if (licence.getAdherent() != null) {
|
||||||
|
newDotation.setFlocage(licence.getAdherent().getNom().toUpperCase() + " " + licence.getAdherent().getPrenom());
|
||||||
|
}
|
||||||
|
currentDotations.add(newDotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
licenceRepository.save(licence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.astalange.core.service;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.Adherent;
|
||||||
|
import com.astalange.core.entity.Licence;
|
||||||
|
import com.astalange.core.entity.PreInscription;
|
||||||
|
import com.astalange.core.repository.AdherentRepository;
|
||||||
|
import com.astalange.core.repository.LicenceRepository;
|
||||||
|
import com.astalange.core.repository.PreInscriptionRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PreInscriptionService {
|
||||||
|
|
||||||
|
private final PreInscriptionRepository preInscriptionRepository;
|
||||||
|
private final AdherentRepository adherentRepository;
|
||||||
|
private final LicenceRepository licenceRepository;
|
||||||
|
|
||||||
|
public PreInscriptionService(PreInscriptionRepository preInscriptionRepository,
|
||||||
|
AdherentRepository adherentRepository,
|
||||||
|
LicenceRepository licenceRepository) {
|
||||||
|
this.preInscriptionRepository = preInscriptionRepository;
|
||||||
|
this.adherentRepository = adherentRepository;
|
||||||
|
this.licenceRepository = licenceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Adherent validerPreInscription(Long preInscriptionId, Long categorieId) {
|
||||||
|
PreInscription pre = preInscriptionRepository.findById(preInscriptionId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Pré-inscription introuvable"));
|
||||||
|
|
||||||
|
if (pre.getStatutTraite()) {
|
||||||
|
throw new IllegalStateException("Cette pré-inscription a déjà été traitée");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'adhérent
|
||||||
|
Adherent adherent = new Adherent();
|
||||||
|
adherent.setNom(pre.getNom().toUpperCase());
|
||||||
|
adherent.setPrenom(pre.getPrenom());
|
||||||
|
adherent.setDateNaissance(pre.getDateNaissance());
|
||||||
|
adherent.setLieuNaissanceVille(pre.getLieuNaissanceVille());
|
||||||
|
adherent.setLieuNaissancePays(pre.getLieuNaissancePays());
|
||||||
|
adherent.setNationalite(pre.getNationalite());
|
||||||
|
adherent.setEmail(pre.getEmail());
|
||||||
|
adherent.setTelephone(pre.getTelephone());
|
||||||
|
adherent.setRepresentantLegal(pre.getRepresentantLegal());
|
||||||
|
adherent.setTypeDemande(pre.getTypeDemande());
|
||||||
|
adherent.setAncienClub(pre.getAncienClub());
|
||||||
|
adherent.setAncienneCategorie(pre.getAncienneCategorie());
|
||||||
|
adherent.setSexe(pre.getSexe());
|
||||||
|
// Par défaut
|
||||||
|
adherent.setTypeMaillot("JOUEUR");
|
||||||
|
adherent.setResidentTalange(false);
|
||||||
|
|
||||||
|
adherent = adherentRepository.save(adherent);
|
||||||
|
|
||||||
|
pre.setStatutTraite(true);
|
||||||
|
preInscriptionRepository.save(pre);
|
||||||
|
|
||||||
|
return adherent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void rejeterPreInscription(Long preInscriptionId) {
|
||||||
|
PreInscription pre = preInscriptionRepository.findById(preInscriptionId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Pré-inscription introuvable"));
|
||||||
|
|
||||||
|
pre.setStatutTraite(true);
|
||||||
|
preInscriptionRepository.save(pre);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,14 +53,15 @@ public class SaisonService {
|
|||||||
private void dupliquerParametrages(Saison ancienneSaison, Saison nouvelleSaison) {
|
private void dupliquerParametrages(Saison ancienneSaison, Saison nouvelleSaison) {
|
||||||
// 1. Dupliquer les équipements
|
// 1. Dupliquer les équipements
|
||||||
List<Equipement> anciensEquipements = equipementRepository.findBySaison(ancienneSaison);
|
List<Equipement> anciensEquipements = equipementRepository.findBySaison(ancienneSaison);
|
||||||
|
java.util.Map<Long, Equipement> oldToNewEquipementMap = new java.util.HashMap<>();
|
||||||
|
|
||||||
for (Equipement ancien : anciensEquipements) {
|
for (Equipement ancien : anciensEquipements) {
|
||||||
Equipement nouvelEquipement = new Equipement();
|
Equipement nouvelEquipement = new Equipement();
|
||||||
nouvelEquipement.setNom(ancien.getNom());
|
nouvelEquipement.setNom(ancien.getNom());
|
||||||
nouvelEquipement.setDescription(ancien.getDescription());
|
nouvelEquipement.setDescription(ancien.getDescription());
|
||||||
nouvelEquipement.setObligatoire(ancien.getObligatoire());
|
|
||||||
nouvelEquipement.setPrixContribution(ancien.getPrixContribution());
|
|
||||||
nouvelEquipement.setSaison(nouvelleSaison);
|
nouvelEquipement.setSaison(nouvelleSaison);
|
||||||
equipementRepository.save(nouvelEquipement);
|
nouvelEquipement = equipementRepository.save(nouvelEquipement);
|
||||||
|
oldToNewEquipementMap.put(ancien.getId(), nouvelEquipement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Dupliquer les catégories (avec incrémentation de l'année)
|
// 2. Dupliquer les catégories (avec incrémentation de l'année)
|
||||||
@@ -78,6 +79,22 @@ public class SaisonService {
|
|||||||
nouvelleCategorie.setTarifBase(ancienne.getTarifBase());
|
nouvelleCategorie.setTarifBase(ancienne.getTarifBase());
|
||||||
nouvelleCategorie.setTarifExterieur(ancienne.getTarifExterieur());
|
nouvelleCategorie.setTarifExterieur(ancienne.getTarifExterieur());
|
||||||
nouvelleCategorie.setSaison(nouvelleSaison);
|
nouvelleCategorie.setSaison(nouvelleSaison);
|
||||||
|
|
||||||
|
// Dupliquer les relations avec les équipements
|
||||||
|
if (ancienne.getCategorieEquipements() != null) {
|
||||||
|
for (com.astalange.core.entity.CategorieEquipement oldCatEq : ancienne.getCategorieEquipements()) {
|
||||||
|
Equipement newEq = oldToNewEquipementMap.get(oldCatEq.getEquipement().getId());
|
||||||
|
if (newEq != null) {
|
||||||
|
com.astalange.core.entity.CategorieEquipement newCatEq = new com.astalange.core.entity.CategorieEquipement();
|
||||||
|
newCatEq.setCategorie(nouvelleCategorie);
|
||||||
|
newCatEq.setEquipement(newEq);
|
||||||
|
newCatEq.setObligatoire(oldCatEq.getObligatoire());
|
||||||
|
newCatEq.setPrix(oldCatEq.getPrix());
|
||||||
|
nouvelleCategorie.getCategorieEquipements().add(newCatEq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
categorieRepository.save(nouvelleCategorie);
|
categorieRepository.save(nouvelleCategorie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- V13__add_equipe_to_educateur.sql
|
||||||
|
ALTER TABLE educateur ADD COLUMN equipe_id BIGINT;
|
||||||
|
ALTER TABLE educateur ADD CONSTRAINT fk_educateur_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE categorie_equipement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
categorie_id BIGINT NOT NULL,
|
||||||
|
equipement_id BIGINT NOT NULL,
|
||||||
|
obligatoire BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT fk_cat_equip_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_cat_equip_equipement FOREIGN KEY (equipement_id) REFERENCES equipement(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrer les données existantes : associer tous les équipements d'une saison à toutes les catégories de la même saison
|
||||||
|
-- et transférer la valeur "obligatoire" de l'équipement vers la nouvelle table de liaison
|
||||||
|
INSERT INTO categorie_equipement (categorie_id, equipement_id, obligatoire)
|
||||||
|
SELECT c.id, e.id, e.obligatoire
|
||||||
|
FROM categorie c
|
||||||
|
JOIN equipement e ON c.saison_id = e.saison_id;
|
||||||
|
|
||||||
|
-- Supprimer la colonne "obligatoire" de la table équipement
|
||||||
|
ALTER TABLE equipement DROP COLUMN obligatoire;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE categorie_equipement ADD COLUMN prix NUMERIC(10,2) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Migrer les données existantes
|
||||||
|
UPDATE categorie_equipement ce
|
||||||
|
SET prix = e.prix_contribution
|
||||||
|
FROM equipement e
|
||||||
|
WHERE ce.equipement_id = e.id;
|
||||||
|
|
||||||
|
ALTER TABLE equipement DROP COLUMN IF EXISTS prix_contribution;
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE token_pre_inscription (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
valeur_uuid VARCHAR(36) NOT NULL UNIQUE,
|
||||||
|
date_expiration TIMESTAMP NOT NULL,
|
||||||
|
est_utilise BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pre_inscription (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
nom VARCHAR(100) NOT NULL,
|
||||||
|
prenom VARCHAR(100) NOT NULL,
|
||||||
|
date_naissance DATE NOT NULL,
|
||||||
|
telephone VARCHAR(20),
|
||||||
|
email VARCHAR(150),
|
||||||
|
adresse TEXT,
|
||||||
|
representant_legal VARCHAR(150),
|
||||||
|
statut_traite BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
saison_id BIGINT NOT NULL,
|
||||||
|
CONSTRAINT fk_pre_inscription_saison FOREIGN KEY (saison_id) REFERENCES saison (id)
|
||||||
|
);
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE saison ADD COLUMN inscriptions_ouvertes BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-- Drop the global unique constraint on 'nom'
|
||||||
|
ALTER TABLE categorie DROP CONSTRAINT IF EXISTS categorie_nom_key;
|
||||||
|
|
||||||
|
-- Add a composite unique constraint for 'nom' and 'saison_id'
|
||||||
|
ALTER TABLE categorie ADD CONSTRAINT categorie_nom_saison_id_key UNIQUE (nom, saison_id);
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-- adherent modifications
|
||||||
|
ALTER TABLE adherent DROP COLUMN IF EXISTS adresse;
|
||||||
|
ALTER TABLE adherent ADD COLUMN lieu_naissance_ville VARCHAR(100);
|
||||||
|
ALTER TABLE adherent ADD COLUMN lieu_naissance_pays VARCHAR(100);
|
||||||
|
ALTER TABLE adherent ADD COLUMN nationalite VARCHAR(100);
|
||||||
|
|
||||||
|
-- pre_inscription modifications
|
||||||
|
ALTER TABLE pre_inscription DROP COLUMN IF EXISTS adresse;
|
||||||
|
ALTER TABLE pre_inscription ADD COLUMN lieu_naissance_ville VARCHAR(100);
|
||||||
|
ALTER TABLE pre_inscription ADD COLUMN lieu_naissance_pays VARCHAR(100);
|
||||||
|
ALTER TABLE pre_inscription ADD COLUMN nationalite VARCHAR(100);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE licence ADD COLUMN type_demande VARCHAR(50);
|
||||||
|
ALTER TABLE licence ADD COLUMN type_licence VARCHAR(50);
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
-- Default values for existing records in adherent
|
||||||
|
UPDATE adherent SET lieu_naissance_ville = 'Non renseigné' WHERE lieu_naissance_ville IS NULL;
|
||||||
|
UPDATE adherent SET lieu_naissance_pays = 'France' WHERE lieu_naissance_pays IS NULL;
|
||||||
|
UPDATE adherent SET nationalite = 'Française' WHERE nationalite IS NULL;
|
||||||
|
|
||||||
|
-- Default values for existing records in pre_inscription
|
||||||
|
UPDATE pre_inscription SET lieu_naissance_ville = 'Non renseigné' WHERE lieu_naissance_ville IS NULL;
|
||||||
|
UPDATE pre_inscription SET lieu_naissance_pays = 'France' WHERE lieu_naissance_pays IS NULL;
|
||||||
|
UPDATE pre_inscription SET nationalite = 'Française' WHERE nationalite IS NULL;
|
||||||
|
|
||||||
|
-- Now make them NOT NULL
|
||||||
|
ALTER TABLE adherent ALTER COLUMN lieu_naissance_ville SET NOT NULL;
|
||||||
|
ALTER TABLE adherent ALTER COLUMN lieu_naissance_pays SET NOT NULL;
|
||||||
|
ALTER TABLE adherent ALTER COLUMN nationalite SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE pre_inscription ALTER COLUMN lieu_naissance_ville SET NOT NULL;
|
||||||
|
ALTER TABLE pre_inscription ALTER COLUMN lieu_naissance_pays SET NOT NULL;
|
||||||
|
ALTER TABLE pre_inscription ALTER COLUMN nationalite SET NOT NULL;
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-- Provide fallback email if null
|
||||||
|
UPDATE adherent SET email = 'nonrenseigne@example.com' WHERE email IS NULL OR email = '';
|
||||||
|
UPDATE pre_inscription SET email = 'nonrenseigne@example.com' WHERE email IS NULL OR email = '';
|
||||||
|
|
||||||
|
-- Make email NOT NULL
|
||||||
|
ALTER TABLE adherent ALTER COLUMN email SET NOT NULL;
|
||||||
|
ALTER TABLE pre_inscription ALTER COLUMN email SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove telephone
|
||||||
|
ALTER TABLE adherent DROP COLUMN IF EXISTS telephone;
|
||||||
|
ALTER TABLE pre_inscription DROP COLUMN IF EXISTS telephone;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE paiement ADD COLUMN gestionnaire VARCHAR(255);
|
||||||
+1
@@ -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);
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE adherent ADD COLUMN ancienne_categorie VARCHAR(50);
|
||||||
|
ALTER TABLE pre_inscription ADD COLUMN ancienne_categorie VARCHAR(50) DEFAULT 'NA';
|
||||||
|
|
||||||
|
ALTER TABLE categorie ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
|
||||||
|
|
||||||
|
UPDATE adherent SET telephone = '0000000000' WHERE telephone IS NULL;
|
||||||
|
ALTER TABLE adherent ALTER COLUMN telephone SET NOT NULL;
|
||||||
|
|
||||||
|
UPDATE pre_inscription SET telephone = '0000000000' WHERE telephone IS NULL;
|
||||||
|
ALTER TABLE pre_inscription ALTER COLUMN telephone SET NOT NULL;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE equipement ADD COLUMN gestion_sexe BOOLEAN DEFAULT FALSE NOT NULL;
|
||||||
|
ALTER TABLE equipement ADD COLUMN tailles_disponibles VARCHAR(500);
|
||||||
|
ALTER TABLE equipement ADD COLUMN couleurs_disponibles VARCHAR(500);
|
||||||
|
|
||||||
|
ALTER TABLE dotation ADD COLUMN couleur VARCHAR(50);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE paiement ADD COLUMN numero_cheque VARCHAR(50);
|
||||||
|
ALTER TABLE paiement ADD COLUMN commentaire TEXT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE equipement ADD COLUMN type_public VARCHAR(20) DEFAULT 'TOUS' NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE pre_inscription ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE dotation ALTER COLUMN taille TYPE VARCHAR(50);
|
||||||
|
ALTER TABLE dotation ALTER COLUMN numero TYPE VARCHAR(50);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.astalange.core;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class EducateurTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEducateurEntityAndAssociations() {
|
||||||
|
Categorie U15 = new Categorie();
|
||||||
|
U15.setNom("U15");
|
||||||
|
|
||||||
|
Equipe equipeA = new Equipe();
|
||||||
|
equipeA.setNom("Équipe A");
|
||||||
|
equipeA.setCategorie(U15);
|
||||||
|
|
||||||
|
Educateur educateur = new Educateur();
|
||||||
|
educateur.setNom("Dupont");
|
||||||
|
educateur.setPrenom("Jean");
|
||||||
|
educateur.setTelephone("0612345678");
|
||||||
|
educateur.setEmail("jean.dupont@example.com");
|
||||||
|
educateur.setCategorie(U15);
|
||||||
|
educateur.setEquipe(equipeA);
|
||||||
|
|
||||||
|
assertEquals("Dupont", educateur.getNom());
|
||||||
|
assertEquals("Jean", educateur.getPrenom());
|
||||||
|
assertEquals("0612345678", educateur.getTelephone());
|
||||||
|
assertEquals("jean.dupont@example.com", educateur.getEmail());
|
||||||
|
assertEquals(U15, educateur.getCategorie());
|
||||||
|
assertEquals(equipeA, educateur.getEquipe());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -25,10 +28,10 @@ public class SecurityConfig {
|
|||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/css/**", "/js/**", "/images/**").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")
|
||||||
@@ -45,7 +47,6 @@ public class AdherentController {
|
|||||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String categorie,
|
@org.springframework.web.bind.annotation.RequestParam(required = false) String categorie,
|
||||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String licence,
|
@org.springframework.web.bind.annotation.RequestParam(required = false) String licence,
|
||||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String email,
|
@org.springframework.web.bind.annotation.RequestParam(required = false) String email,
|
||||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String telephone,
|
|
||||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String paiement,
|
@org.springframework.web.bind.annotation.RequestParam(required = false) String paiement,
|
||||||
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "0") int page,
|
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "0") int page,
|
||||||
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "20") int size,
|
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "20") int size,
|
||||||
@@ -97,11 +98,6 @@ public class AdherentController {
|
|||||||
adherents = adherents.stream().filter(a -> a.getEmail() != null && a.getEmail().toLowerCase().contains(e)).collect(java.util.stream.Collectors.toList());
|
adherents = adherents.stream().filter(a -> a.getEmail() != null && a.getEmail().toLowerCase().contains(e)).collect(java.util.stream.Collectors.toList());
|
||||||
model.addAttribute("emailFilter", email.trim());
|
model.addAttribute("emailFilter", email.trim());
|
||||||
}
|
}
|
||||||
if (telephone != null && !telephone.trim().isEmpty()) {
|
|
||||||
String t = telephone.trim().toLowerCase();
|
|
||||||
adherents = adherents.stream().filter(a -> a.getTelephone() != null && a.getTelephone().toLowerCase().contains(t)).collect(java.util.stream.Collectors.toList());
|
|
||||||
model.addAttribute("telephoneFilter", telephone.trim());
|
|
||||||
}
|
|
||||||
if (paiement != null && !paiement.trim().isEmpty()) {
|
if (paiement != null && !paiement.trim().isEmpty()) {
|
||||||
adherents = adherents.stream().filter(a -> {
|
adherents = adherents.stream().filter(a -> {
|
||||||
String statut = a.getStatutPaiement();
|
String statut = a.getStatutPaiement();
|
||||||
@@ -181,29 +177,39 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
existing.setNom(adherent.getNom());
|
existing.setNom(adherent.getNom());
|
||||||
existing.setPrenom(adherent.getPrenom());
|
existing.setPrenom(adherent.getPrenom());
|
||||||
existing.setDateNaissance(adherent.getDateNaissance());
|
existing.setDateNaissance(adherent.getDateNaissance());
|
||||||
existing.setTelephone(adherent.getTelephone());
|
existing.setLieuNaissanceVille(adherent.getLieuNaissanceVille());
|
||||||
|
existing.setLieuNaissancePays(adherent.getLieuNaissancePays());
|
||||||
|
existing.setNationalite(adherent.getNationalite());
|
||||||
existing.setEmail(adherent.getEmail());
|
existing.setEmail(adherent.getEmail());
|
||||||
existing.setAdresse(adherent.getAdresse());
|
|
||||||
existing.setRepresentantLegal(adherent.getRepresentantLegal());
|
existing.setRepresentantLegal(adherent.getRepresentantLegal());
|
||||||
existing.setResidentTalange(adherent.isResidentTalange());
|
existing.setResidentTalange(adherent.isResidentTalange());
|
||||||
existing.setSexe(adherent.getSexe());
|
existing.setSexe(adherent.getSexe());
|
||||||
@@ -213,11 +219,11 @@ public class AdherentController {
|
|||||||
} else {
|
} else {
|
||||||
adherentRepository.save(adherent);
|
adherentRepository.save(adherent);
|
||||||
}
|
}
|
||||||
|
return "redirect:/adherents";
|
||||||
} else {
|
} else {
|
||||||
adherentRepository.save(adherent);
|
Adherent saved = adherentRepository.save(adherent);
|
||||||
|
return "redirect:/adherents/" + saved.getId() + "/edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/adherents";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/edit")
|
@GetMapping("/{id}/edit")
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.Adherent;
|
||||||
|
import com.astalange.core.repository.PreInscriptionRepository;
|
||||||
|
import com.astalange.core.service.PreInscriptionService;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/admin/pre-inscriptions")
|
||||||
|
public class AdminPreInscriptionController {
|
||||||
|
|
||||||
|
private final PreInscriptionRepository preInscriptionRepository;
|
||||||
|
private final PreInscriptionService preInscriptionService;
|
||||||
|
|
||||||
|
public AdminPreInscriptionController(PreInscriptionRepository preInscriptionRepository,
|
||||||
|
PreInscriptionService preInscriptionService) {
|
||||||
|
this.preInscriptionRepository = preInscriptionRepository;
|
||||||
|
this.preInscriptionService = preInscriptionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String listPreInscriptions(Model model) {
|
||||||
|
model.addAttribute("preInscriptions", preInscriptionRepository.findByStatutTraiteFalseOrderByDateCreationDesc());
|
||||||
|
return "preinscriptions/list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/valider")
|
||||||
|
@ResponseBody
|
||||||
|
public String valider(@PathVariable Long id, HttpServletResponse response) {
|
||||||
|
Adherent adherent = preInscriptionService.validerPreInscription(id, null);
|
||||||
|
response.setHeader("HX-Trigger", "refreshBadge");
|
||||||
|
// En HTMX, on renvoie une ligne vide pour la faire disparaître, ou on redirige via HX-Redirect
|
||||||
|
// Mais comme on veut aller sur la page de l'adhérent pour finaliser la licence :
|
||||||
|
response.setHeader("HX-Redirect", "/adherents/" + adherent.getId() + "/edit");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/rejeter")
|
||||||
|
@ResponseBody
|
||||||
|
public String rejeter(@PathVariable Long id, HttpServletResponse response) {
|
||||||
|
preInscriptionService.rejeterPreInscription(id);
|
||||||
|
response.setHeader("HX-Trigger", "refreshBadge");
|
||||||
|
return ""; // HTMX target la ligne avec outerHTML -> supprime la ligne
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/count")
|
||||||
|
@ResponseBody
|
||||||
|
public String countBadge() {
|
||||||
|
long count = preInscriptionRepository.countByStatutTraiteFalse();
|
||||||
|
if (count == 0) return "";
|
||||||
|
return "<span class=\"bg-red-500 text-white rounded-full px-2 py-0.5 text-xs font-bold ml-2\">" + count + "</span>";
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
package com.astalange.web.controller;
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
import com.astalange.core.entity.Categorie;
|
import com.astalange.core.entity.Categorie;
|
||||||
|
import com.astalange.core.entity.Saison;
|
||||||
import com.astalange.core.repository.CategorieRepository;
|
import com.astalange.core.repository.CategorieRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
|
import com.astalange.core.repository.EquipementRepository;
|
||||||
|
import com.astalange.core.service.CategorieService;
|
||||||
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 java.util.List;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/categories")
|
@RequestMapping("/categories")
|
||||||
public class CategorieController {
|
public class CategorieController {
|
||||||
|
|
||||||
private final CategorieRepository categorieRepository;
|
private final CategorieRepository categorieRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
private final EquipementRepository equipementRepository;
|
||||||
|
private final CategorieService categorieService;
|
||||||
|
|
||||||
public CategorieController(CategorieRepository categorieRepository) {
|
public CategorieController(CategorieRepository categorieRepository, SaisonRepository saisonRepository, EquipementRepository equipementRepository, CategorieService categorieService) {
|
||||||
this.categorieRepository = categorieRepository;
|
this.categorieRepository = categorieRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
|
this.equipementRepository = equipementRepository;
|
||||||
|
this.categorieService = categorieService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -25,6 +37,8 @@ public class CategorieController {
|
|||||||
@GetMapping("/new")
|
@GetMapping("/new")
|
||||||
public String showCreateForm(Model model) {
|
public String showCreateForm(Model model) {
|
||||||
model.addAttribute("categorie", new Categorie());
|
model.addAttribute("categorie", new Categorie());
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of());
|
||||||
return "parametrage/categories_form";
|
return "parametrage/categories_form";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,12 +47,42 @@ 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 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie) {
|
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie,
|
||||||
categorieRepository.save(categorie);
|
jakarta.servlet.http.HttpServletRequest request) {
|
||||||
|
|
||||||
|
java.util.Map<Long, com.astalange.core.service.CategorieService.EquipementConfig> configs = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
request.getParameterMap().forEach((key, values) -> {
|
||||||
|
if (key.startsWith("etatEquipement_") && values.length > 0) {
|
||||||
|
String idStr = key.substring("etatEquipement_".length());
|
||||||
|
try {
|
||||||
|
Long eqId = Long.parseLong(idStr);
|
||||||
|
String etat = values[0];
|
||||||
|
if (!"NON_LIE".equals(etat)) {
|
||||||
|
boolean obligatoire = "OBLIGATOIRE".equals(etat);
|
||||||
|
String prixStr = request.getParameter("prixEquipement_" + eqId);
|
||||||
|
java.math.BigDecimal prix = java.math.BigDecimal.ZERO;
|
||||||
|
if (prixStr != null && !prixStr.trim().isEmpty()) {
|
||||||
|
prix = new java.math.BigDecimal(prixStr.trim());
|
||||||
|
}
|
||||||
|
configs.put(eqId, new com.astalange.core.service.CategorieService.EquipementConfig(obligatoire, prix));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
categorieService.saveCategorie(categorie, configs);
|
||||||
return "redirect:/categories";
|
return "redirect:/categories";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -38,7 +145,7 @@ public class DotationController {
|
|||||||
dotation.setNumero(numero);
|
dotation.setNumero(numero);
|
||||||
dotation.setFourni(fourni != null && fourni);
|
dotation.setFourni(fourni != null && fourni);
|
||||||
|
|
||||||
if (dotation.getEquipement().getObligatoire()) {
|
if (dotation.getLicence().getCategorie().isEquipementObligatoire(dotation.getEquipement().getId())) {
|
||||||
dotation.setChoisi(true);
|
dotation.setChoisi(true);
|
||||||
} else {
|
} else {
|
||||||
dotation.setChoisi(choisi != null && choisi);
|
dotation.setChoisi(choisi != null && choisi);
|
||||||
@@ -61,16 +168,18 @@ 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);
|
||||||
|
|
||||||
if (dotation.getEquipement().getObligatoire()) {
|
if (dotation.getLicence().getCategorie().isEquipementObligatoire(dotation.getEquipement().getId())) {
|
||||||
dotation.setChoisi(true);
|
dotation.setChoisi(true);
|
||||||
} else {
|
} else {
|
||||||
dotation.setChoisi(choisiParam != null);
|
dotation.setChoisi(choisiParam != null);
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.Educateur;
|
||||||
|
import com.astalange.core.entity.Categorie;
|
||||||
|
import com.astalange.core.entity.Equipe;
|
||||||
|
import com.astalange.core.entity.Saison;
|
||||||
|
import com.astalange.core.repository.EducateurRepository;
|
||||||
|
import com.astalange.core.repository.CategorieRepository;
|
||||||
|
import com.astalange.core.repository.EquipeRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/educateurs")
|
||||||
|
public class EducateurController {
|
||||||
|
|
||||||
|
private final EducateurRepository educateurRepository;
|
||||||
|
private final CategorieRepository categorieRepository;
|
||||||
|
private final EquipeRepository equipeRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
|
||||||
|
public EducateurController(EducateurRepository educateurRepository,
|
||||||
|
CategorieRepository categorieRepository,
|
||||||
|
EquipeRepository equipeRepository,
|
||||||
|
SaisonRepository saisonRepository) {
|
||||||
|
this.educateurRepository = educateurRepository;
|
||||||
|
this.categorieRepository = categorieRepository;
|
||||||
|
this.equipeRepository = equipeRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String listEducateurs(Model model) {
|
||||||
|
model.addAttribute("educateurs", educateurRepository.findAllWithCategorieAndEquipe());
|
||||||
|
return "parametrage/educateurs_list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/new")
|
||||||
|
public String showCreateForm(Model model) {
|
||||||
|
model.addAttribute("educateur", new Educateur());
|
||||||
|
populateModelAttributes(model);
|
||||||
|
return "parametrage/educateurs_form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/edit")
|
||||||
|
public String showEditForm(@PathVariable Long id, Model model) {
|
||||||
|
Educateur educateur = educateurRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid educator ID: " + id));
|
||||||
|
model.addAttribute("educateur", educateur);
|
||||||
|
populateModelAttributes(model);
|
||||||
|
return "parametrage/educateurs_form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String saveEducateur(@ModelAttribute("educateur") Educateur educateur) {
|
||||||
|
if (educateur.getCategorie() != null && educateur.getCategorie().getId() == null) {
|
||||||
|
educateur.setCategorie(null);
|
||||||
|
}
|
||||||
|
if (educateur.getEquipe() != null && educateur.getEquipe().getId() == null) {
|
||||||
|
educateur.setEquipe(null);
|
||||||
|
}
|
||||||
|
educateurRepository.save(educateur);
|
||||||
|
return "redirect:/educateurs";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/delete")
|
||||||
|
public String deleteEducateur(@PathVariable Long id) {
|
||||||
|
Educateur educateur = educateurRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid educator ID: " + id));
|
||||||
|
educateurRepository.delete(educateur);
|
||||||
|
return "redirect:/educateurs";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateModelAttributes(Model model) {
|
||||||
|
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
if (saisonActive != null) {
|
||||||
|
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
|
||||||
|
model.addAttribute("equipes", equipeRepository.findBySaison(saisonActive));
|
||||||
|
} else {
|
||||||
|
model.addAttribute("categories", categorieRepository.findAll());
|
||||||
|
model.addAttribute("equipes", equipeRepository.findAllWithCategorie());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-4
@@ -1,7 +1,9 @@
|
|||||||
package com.astalange.web.controller;
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
import com.astalange.core.entity.Equipement;
|
import com.astalange.core.entity.Equipement;
|
||||||
|
import com.astalange.core.entity.Saison;
|
||||||
import com.astalange.core.repository.EquipementRepository;
|
import com.astalange.core.repository.EquipementRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
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.*;
|
||||||
@@ -11,14 +13,21 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class EquipementController {
|
public class EquipementController {
|
||||||
|
|
||||||
private final EquipementRepository equipementRepository;
|
private final EquipementRepository equipementRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
|
||||||
public EquipementController(EquipementRepository equipementRepository) {
|
public EquipementController(EquipementRepository equipementRepository, SaisonRepository saisonRepository) {
|
||||||
this.equipementRepository = equipementRepository;
|
this.equipementRepository = equipementRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +47,15 @@ public class EquipementController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public String saveEquipement(@ModelAttribute("equipement") Equipement equipement) {
|
public String saveEquipement(@ModelAttribute("equipement") Equipement equipement) {
|
||||||
if (equipement.getObligatoire() == null) {
|
|
||||||
equipement.setObligatoire(false);
|
if (equipement.getId() == null) {
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
|
||||||
|
equipement.setSaison(activeSaison);
|
||||||
|
} else {
|
||||||
|
Equipement existing = equipementRepository.findById(equipement.getId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + equipement.getId()));
|
||||||
|
equipement.setSaison(existing.getSaison());
|
||||||
}
|
}
|
||||||
equipementRepository.save(equipement);
|
equipementRepository.save(equipement);
|
||||||
return "redirect:/equipements";
|
return "redirect:/equipements";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ 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 org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@Transactional
|
||||||
public class LicenceController {
|
public class LicenceController {
|
||||||
|
|
||||||
private final AdherentRepository adherentRepository;
|
private final AdherentRepository adherentRepository;
|
||||||
@@ -19,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,
|
||||||
@@ -26,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;
|
||||||
@@ -34,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")
|
||||||
@@ -42,6 +47,8 @@ public class LicenceController {
|
|||||||
@RequestParam Long categorieId,
|
@RequestParam Long categorieId,
|
||||||
@RequestParam String etat,
|
@RequestParam String etat,
|
||||||
@RequestParam(required = false) String numeroLicence,
|
@RequestParam(required = false) String numeroLicence,
|
||||||
|
@RequestParam(required = false) String typeDemande,
|
||||||
|
@RequestParam(required = false) String typeLicence,
|
||||||
@RequestParam(required = false) Long equipeId) {
|
@RequestParam(required = false) Long equipeId) {
|
||||||
|
|
||||||
Adherent adherent = adherentRepository.findById(id)
|
Adherent adherent = adherentRepository.findById(id)
|
||||||
@@ -64,6 +71,8 @@ public class LicenceController {
|
|||||||
licence.setSaison(saisonActive);
|
licence.setSaison(saisonActive);
|
||||||
licence.setEtat(etat);
|
licence.setEtat(etat);
|
||||||
licence.setNumeroLicence(numeroLicence);
|
licence.setNumeroLicence(numeroLicence);
|
||||||
|
licence.setTypeDemande(typeDemande);
|
||||||
|
licence.setTypeLicence(typeLicence);
|
||||||
|
|
||||||
if (equipeId != null) {
|
if (equipeId != null) {
|
||||||
Equipe equipe = equipeRepository.findById(equipeId)
|
Equipe equipe = equipeRepository.findById(equipeId)
|
||||||
@@ -73,19 +82,8 @@ public class LicenceController {
|
|||||||
|
|
||||||
licence = licenceRepository.save(licence);
|
licence = licenceRepository.save(licence);
|
||||||
|
|
||||||
// Auto-initialize dotations for all configured equipments of the active season
|
// Auto-initialize dotations via CategorieService to ensure consistency
|
||||||
List<Equipement> equipements = equipementRepository.findBySaison(saisonActive);
|
categorieService.syncDotationsForLicence(licence);
|
||||||
for (Equipement eq : equipements) {
|
|
||||||
Dotation dotation = new Dotation();
|
|
||||||
dotation.setLicence(licence);
|
|
||||||
dotation.setEquipement(eq);
|
|
||||||
dotation.setTaille("");
|
|
||||||
dotation.setFlocage(adherent.getNom().toUpperCase() + " " + adherent.getPrenom());
|
|
||||||
dotation.setNumero("");
|
|
||||||
dotation.setFourni(false);
|
|
||||||
dotation.setChoisi(eq.getObligatoire());
|
|
||||||
dotationRepository.save(dotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "redirect:/adherents/" + id + "/edit";
|
return "redirect:/adherents/" + id + "/edit";
|
||||||
}
|
}
|
||||||
@@ -97,6 +95,8 @@ public class LicenceController {
|
|||||||
@RequestParam Long categorieId,
|
@RequestParam Long categorieId,
|
||||||
@RequestParam String etat,
|
@RequestParam String etat,
|
||||||
@RequestParam(required = false) String numeroLicence,
|
@RequestParam(required = false) String numeroLicence,
|
||||||
|
@RequestParam(required = false) String typeDemande,
|
||||||
|
@RequestParam(required = false) String typeLicence,
|
||||||
@RequestParam(required = false) Long equipeId) {
|
@RequestParam(required = false) Long equipeId) {
|
||||||
|
|
||||||
Licence licence = licenceRepository.findById(licenceId)
|
Licence licence = licenceRepository.findById(licenceId)
|
||||||
@@ -108,6 +108,8 @@ public class LicenceController {
|
|||||||
licence.setCategorie(categorie);
|
licence.setCategorie(categorie);
|
||||||
licence.setEtat(etat);
|
licence.setEtat(etat);
|
||||||
licence.setNumeroLicence(numeroLicence);
|
licence.setNumeroLicence(numeroLicence);
|
||||||
|
licence.setTypeDemande(typeDemande);
|
||||||
|
licence.setTypeLicence(typeLicence);
|
||||||
|
|
||||||
if (equipeId != null) {
|
if (equipeId != null) {
|
||||||
Equipe equipe = equipeRepository.findById(equipeId)
|
Equipe equipe = equipeRepository.findById(equipeId)
|
||||||
@@ -117,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,9 +60,106 @@ 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/paiements/{id}/update")
|
||||||
|
public String updatePaiement(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam BigDecimal montant,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
|
||||||
|
@RequestParam Long modePaiementId,
|
||||||
|
@RequestParam(required = false) String numeroCheque,
|
||||||
|
@RequestParam(required = false) String commentaire,
|
||||||
|
@RequestParam(required = false) String redirect,
|
||||||
|
Principal principal) {
|
||||||
|
|
||||||
|
Paiement paiement = paiementRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid Paiement ID"));
|
||||||
|
|
||||||
|
ModePaiement mode = modePaiementRepository.findById(modePaiementId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid ModePaiement ID"));
|
||||||
|
|
||||||
|
BigDecimal ancienMontant = paiement.getMontant();
|
||||||
|
String ancienMode = paiement.getModePaiement().getNom();
|
||||||
|
|
||||||
|
Licence licence = paiement.getLicence();
|
||||||
|
|
||||||
|
// Validation basique pour ne pas dépasser le montant total de la licence
|
||||||
|
BigDecimal maxAmount = licence.getResteAPayer().add(paiement.getMontant());
|
||||||
|
if (montant.compareTo(maxAmount) > 0) {
|
||||||
|
montant = maxAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (montant.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
paiement.setModePaiement(mode);
|
||||||
|
paiement.setMontant(montant);
|
||||||
|
paiement.setDatePaiement(datePaiement);
|
||||||
|
paiement.setNumeroCheque(numeroCheque);
|
||||||
|
paiement.setCommentaire(commentaire);
|
||||||
|
if (principal != null) {
|
||||||
|
paiement.setGestionnaire(principal.getName());
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
|
return "redirect:" + redirect;
|
||||||
|
}
|
||||||
|
return "redirect:/adherents/" + licence.getAdherent().getId() + "/edit";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/paiements/{id}/delete")
|
||||||
|
public String deletePaiement(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(required = false) String redirect,
|
||||||
|
Principal principal) {
|
||||||
|
|
||||||
|
Paiement paiement = paiementRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid Paiement ID"));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (redirect != null && !redirect.isEmpty()) {
|
||||||
|
return "redirect:" + redirect;
|
||||||
|
}
|
||||||
|
return "redirect:/adherents/" + adherentId + "/edit";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.astalange.web.controller;
|
|||||||
import com.astalange.core.entity.*;
|
import com.astalange.core.entity.*;
|
||||||
import com.astalange.core.repository.CategorieRepository;
|
import com.astalange.core.repository.CategorieRepository;
|
||||||
import com.astalange.core.repository.CreneauEntrainementRepository;
|
import com.astalange.core.repository.CreneauEntrainementRepository;
|
||||||
|
import com.astalange.core.repository.EducateurRepository;
|
||||||
import com.astalange.core.repository.EquipeRepository;
|
import com.astalange.core.repository.EquipeRepository;
|
||||||
import com.astalange.core.repository.SaisonRepository;
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
import com.astalange.core.repository.TerrainRepository;
|
import com.astalange.core.repository.TerrainRepository;
|
||||||
@@ -23,17 +24,20 @@ public class PlanningController {
|
|||||||
private final CategorieRepository categorieRepository;
|
private final CategorieRepository categorieRepository;
|
||||||
private final SaisonRepository saisonRepository;
|
private final SaisonRepository saisonRepository;
|
||||||
private final EquipeRepository equipeRepository;
|
private final EquipeRepository equipeRepository;
|
||||||
|
private final EducateurRepository educateurRepository;
|
||||||
|
|
||||||
public PlanningController(CreneauEntrainementRepository creneauRepository,
|
public PlanningController(CreneauEntrainementRepository creneauRepository,
|
||||||
TerrainRepository terrainRepository,
|
TerrainRepository terrainRepository,
|
||||||
CategorieRepository categorieRepository,
|
CategorieRepository categorieRepository,
|
||||||
SaisonRepository saisonRepository,
|
SaisonRepository saisonRepository,
|
||||||
EquipeRepository equipeRepository) {
|
EquipeRepository equipeRepository,
|
||||||
|
EducateurRepository educateurRepository) {
|
||||||
this.creneauRepository = creneauRepository;
|
this.creneauRepository = creneauRepository;
|
||||||
this.terrainRepository = terrainRepository;
|
this.terrainRepository = terrainRepository;
|
||||||
this.categorieRepository = categorieRepository;
|
this.categorieRepository = categorieRepository;
|
||||||
this.saisonRepository = saisonRepository;
|
this.saisonRepository = saisonRepository;
|
||||||
this.equipeRepository = equipeRepository;
|
this.equipeRepository = equipeRepository;
|
||||||
|
this.educateurRepository = educateurRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -56,10 +60,33 @@ public class PlanningController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<CreneauEntrainement> creneaux = creneauRepository.findBySaisonAndJourSemaineFetch(saisonActive, selectedDay);
|
List<CreneauEntrainement> creneaux = creneauRepository.findBySaisonAndJourSemaineFetch(saisonActive, selectedDay);
|
||||||
|
List<Educateur> educateurs = educateurRepository.findAllWithCategorieAndEquipe();
|
||||||
|
|
||||||
// Detect conflicts
|
// Detect conflicts and populate educator initials
|
||||||
for (int i = 0; i < creneaux.size(); i++) {
|
for (int i = 0; i < creneaux.size(); i++) {
|
||||||
CreneauEntrainement c1 = creneaux.get(i);
|
CreneauEntrainement c1 = creneaux.get(i);
|
||||||
|
|
||||||
|
// Populate educator initials
|
||||||
|
List<String> initials = new ArrayList<>();
|
||||||
|
for (Educateur ed : educateurs) {
|
||||||
|
boolean match = false;
|
||||||
|
if (c1.getEquipe() != null) {
|
||||||
|
if (ed.getEquipe() != null && ed.getEquipe().getId().equals(c1.getEquipe().getId())) {
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
} else if (c1.getCategorie() != null) {
|
||||||
|
if (ed.getCategorie() != null && ed.getCategorie().getId().equals(c1.getCategorie().getId())) {
|
||||||
|
match = true;
|
||||||
|
} else if (ed.getEquipe() != null && ed.getEquipe().getCategorie() != null && ed.getEquipe().getCategorie().getId().equals(c1.getCategorie().getId())) {
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
initials.add(ed.getInitiales());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c1.setInitialesEducateurs(String.join(", ", initials));
|
||||||
|
|
||||||
for (int j = 0; j < creneaux.size(); j++) {
|
for (int j = 0; j < creneaux.size(); j++) {
|
||||||
if (i == j) continue;
|
if (i == j) continue;
|
||||||
CreneauEntrainement c2 = creneaux.get(j);
|
CreneauEntrainement c2 = creneaux.get(j);
|
||||||
|
|||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.PreInscription;
|
||||||
|
import com.astalange.core.entity.TokenPreInscription;
|
||||||
|
import com.astalange.core.entity.Saison;
|
||||||
|
import com.astalange.core.repository.PreInscriptionRepository;
|
||||||
|
import com.astalange.core.repository.TokenPreInscriptionRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
|
import com.astalange.core.repository.CategorieRepository;
|
||||||
|
import com.astalange.core.entity.Categorie;
|
||||||
|
import com.astalange.core.service.CaptchaValidationService;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.List;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/inscription-public")
|
||||||
|
public class PublicInscriptionController {
|
||||||
|
|
||||||
|
private final CaptchaValidationService captchaValidationService;
|
||||||
|
private final TokenPreInscriptionRepository tokenRepository;
|
||||||
|
private final PreInscriptionRepository preInscriptionRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
private final CategorieRepository categorieRepository;
|
||||||
|
|
||||||
|
public PublicInscriptionController(CaptchaValidationService captchaValidationService,
|
||||||
|
TokenPreInscriptionRepository tokenRepository,
|
||||||
|
PreInscriptionRepository preInscriptionRepository,
|
||||||
|
SaisonRepository saisonRepository,
|
||||||
|
CategorieRepository categorieRepository) {
|
||||||
|
this.captchaValidationService = captchaValidationService;
|
||||||
|
this.tokenRepository = tokenRepository;
|
||||||
|
this.preInscriptionRepository = preInscriptionRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
|
this.categorieRepository = categorieRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value("${captcha.sitekey:1x00000000000000000000AA}")
|
||||||
|
private String captchaSiteKey;
|
||||||
|
|
||||||
|
@GetMapping("/demarrer")
|
||||||
|
public String showDemarrer(Model model) {
|
||||||
|
model.addAttribute("captchaSiteKey", captchaSiteKey);
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
|
||||||
|
model.addAttribute("inscriptionsFermees", true);
|
||||||
|
}
|
||||||
|
return "public/demarrer";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/demarrer")
|
||||||
|
public String processDemarrer(@RequestParam("cf-turnstile-response") String captchaResponse,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
|
||||||
|
redirectAttributes.addFlashAttribute("error",
|
||||||
|
"Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
|
||||||
|
return "redirect:/inscription-public/demarrer";
|
||||||
|
}
|
||||||
|
// En vrai, captchaResponse dépend du fournisseur. Cloudflare Turnstile =
|
||||||
|
// cf-turnstile-response
|
||||||
|
if (!captchaValidationService.validateCaptcha(captchaResponse)) {
|
||||||
|
redirectAttributes.addFlashAttribute("error", "Validation CAPTCHA échouée. Veuillez réessayer.");
|
||||||
|
return "redirect:/inscription-public/demarrer";
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenPreInscription token = new TokenPreInscription();
|
||||||
|
token.setValeurUuid(UUID.randomUUID().toString());
|
||||||
|
token.setDateExpiration(LocalDateTime.now().plusMinutes(15));
|
||||||
|
tokenRepository.save(token);
|
||||||
|
|
||||||
|
return "redirect:/inscription-public/formulaire?token=" + token.getValeurUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/formulaire")
|
||||||
|
public String showFormulaire(@RequestParam("token") String tokenUuid, Model model,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
|
||||||
|
redirectAttributes.addFlashAttribute("error",
|
||||||
|
"Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
|
||||||
|
return "redirect:/inscription-public/demarrer";
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
|
||||||
|
if (token == null || !token.isValide()) {
|
||||||
|
return "public/lien-expire";
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("preInscription", new PreInscription());
|
||||||
|
model.addAttribute("tokenUuid", tokenUuid);
|
||||||
|
return "public/formulaire";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/formulaire")
|
||||||
|
public String processFormulaire(@RequestParam("token") String tokenUuid,
|
||||||
|
@ModelAttribute("preInscription") PreInscription preInscription,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
TokenPreInscription token = tokenRepository.findByValeurUuid(tokenUuid).orElse(null);
|
||||||
|
if (token == null || !token.isValide()) {
|
||||||
|
return "public/lien-expire";
|
||||||
|
}
|
||||||
|
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
if (activeSaison == null || !activeSaison.getInscriptionsOuvertes()) {
|
||||||
|
redirectAttributes.addFlashAttribute("error",
|
||||||
|
"Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.");
|
||||||
|
return "redirect:/inscription-public/demarrer";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sauvegarde de la pré-inscription
|
||||||
|
preInscription.setSaison(activeSaison);
|
||||||
|
preInscription.setStatutTraite(false);
|
||||||
|
preInscriptionRepository.save(preInscription);
|
||||||
|
|
||||||
|
// Invalidation du token
|
||||||
|
token.setEstUtilise(true);
|
||||||
|
tokenRepository.save(token);
|
||||||
|
|
||||||
|
if ("NOUVELLE".equals(preInscription.getTypeDemande())) {
|
||||||
|
String tarifStr = "210 €"; // Default
|
||||||
|
if (preInscription.getDateNaissance() != null) {
|
||||||
|
int annee = preInscription.getDateNaissance().getYear();
|
||||||
|
List<Categorie> categories = categorieRepository.findBySaisonAndAnnee(activeSaison, annee);
|
||||||
|
if (!categories.isEmpty()) {
|
||||||
|
Categorie cat = categories.get(0);
|
||||||
|
if (cat.getTarifBase() != null) {
|
||||||
|
BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
|
||||||
|
if (prixMaillot == null) {
|
||||||
|
prixMaillot = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
|
||||||
|
tarifStr = tarifTotal.intValue() + " €";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectAttributes.addFlashAttribute("prenom", preInscription.getPrenom());
|
||||||
|
redirectAttributes.addFlashAttribute("tarif", tarifStr);
|
||||||
|
redirectAttributes.addFlashAttribute("documents", java.util.Arrays.asList(
|
||||||
|
"Photo d’identité",
|
||||||
|
"Carte d’identité ou passeport de l'adhérent",
|
||||||
|
"Livret de famille (si pas de pièce d’identité)",
|
||||||
|
"Adresse e-mail de l'adhérent (représentant légal en cas de mineur)",
|
||||||
|
"Pour les adhérents nés à l'étranger : Passeport + justificatif de domicile (du représentant légal en cas de mineur)"
|
||||||
|
));
|
||||||
|
redirectAttributes.addFlashAttribute("isNouvelle", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "redirect:/inscription-public/succes";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/tarif")
|
||||||
|
@ResponseBody
|
||||||
|
public String getTarif(@RequestParam(value = "dateNaissance", required = false) String dateNaissanceStr) {
|
||||||
|
if (dateNaissanceStr == null || dateNaissanceStr.isEmpty()) {
|
||||||
|
return "210 €";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LocalDate dateNaissance = LocalDate.parse(dateNaissanceStr);
|
||||||
|
int annee = dateNaissance.getYear();
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
if (activeSaison == null) return "210 €";
|
||||||
|
|
||||||
|
List<Categorie> categories = categorieRepository.findBySaisonAndAnnee(activeSaison, annee);
|
||||||
|
if (categories.isEmpty()) return "210 €";
|
||||||
|
|
||||||
|
Categorie cat = categories.get(0);
|
||||||
|
if (cat.getTarifBase() != null) {
|
||||||
|
BigDecimal prixMaillot = cat.getEquipementPrixByNom("Maillot");
|
||||||
|
if (prixMaillot == null) {
|
||||||
|
prixMaillot = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
BigDecimal tarifTotal = cat.getTarifBase().add(prixMaillot);
|
||||||
|
return tarifTotal.intValue() + " €";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Return default on error
|
||||||
|
}
|
||||||
|
return "210 €";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/succes")
|
||||||
|
public String showSucces() {
|
||||||
|
return "public/succes";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,4 +58,13 @@ public class SaisonController {
|
|||||||
|
|
||||||
return "redirect:/saisons";
|
return "redirect:/saisons";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/toggle-inscriptions")
|
||||||
|
public String toggleInscriptions(@PathVariable Long id) {
|
||||||
|
Saison saison = saisonRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Saison introuvable : " + id));
|
||||||
|
saison.setInscriptionsOuvertes(!saison.getInscriptionsOuvertes());
|
||||||
|
saisonRepository.save(saison);
|
||||||
|
return "redirect:/saisons";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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}" />
|
||||||
@@ -64,16 +64,38 @@
|
|||||||
<p th:if="${#fields.hasErrors('dateNaissance')}" th:errors="*{dateNaissance}" class="mt-1 text-xs text-red-600"></p>
|
<p th:if="${#fields.hasErrors('dateNaissance')}" th:errors="*{dateNaissance}" class="mt-1 text-xs text-red-600"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Téléphone -->
|
<!-- Lieu de naissance (Ville) -->
|
||||||
<div>
|
<div>
|
||||||
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
|
<label for="lieuNaissanceVille" class="block text-sm font-medium text-gray-700 mb-1">Ville de naissance <span class="text-red-500">*</span></label>
|
||||||
<input type="tel" id="telephone" th:field="*{telephone}"
|
<input type="text" id="lieuNaissanceVille" th:field="*{lieuNaissanceVille}" required
|
||||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
th:classappend="${#fields.hasErrors('telephone')} ? 'border-red-500' : ''">
|
th:classappend="${#fields.hasErrors('lieuNaissanceVille')} ? 'border-red-500' : ''">
|
||||||
<p th:if="${#fields.hasErrors('telephone')}" th:errors="*{telephone}" class="mt-1 text-xs text-red-600"></p>
|
<p th:if="${#fields.hasErrors('lieuNaissanceVille')}" th:errors="*{lieuNaissanceVille}" class="mt-1 text-xs text-red-600"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Lieu de naissance (Pays) -->
|
||||||
|
<div>
|
||||||
|
<label for="lieuNaissancePays" class="block text-sm font-medium text-gray-700 mb-1">Pays de naissance <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="lieuNaissancePays" th:field="*{lieuNaissancePays}" 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('lieuNaissancePays')} ? 'border-red-500' : ''">
|
||||||
|
<p th:if="${#fields.hasErrors('lieuNaissancePays')}" th:errors="*{lieuNaissancePays}" class="mt-1 text-xs text-red-600"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nationalité -->
|
||||||
|
<div>
|
||||||
|
<label for="nationalite" class="block text-sm font-medium text-gray-700 mb-1">Nationalité <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="nationalite" th:field="*{nationalite}" 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('nationalite')} ? 'border-red-500' : ''">
|
||||||
|
<p th:if="${#fields.hasErrors('nationalite')}" th:errors="*{nationalite}" class="mt-1 text-xs text-red-600"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Removed telephone -->
|
||||||
|
|
||||||
<!-- Représentant Légal (Dynamique) -->
|
<!-- Représentant Légal (Dynamique) -->
|
||||||
<div id="representantBlock" class="hidden-block bg-blue-50 p-4 rounded-lg border border-blue-100">
|
<div id="representantBlock" class="hidden-block bg-blue-50 p-4 rounded-lg border border-blue-100">
|
||||||
<label for="representantLegal" class="block text-sm font-medium text-blue-800 mb-1">
|
<label for="representantLegal" class="block text-sm font-medium text-blue-800 mb-1">
|
||||||
@@ -89,19 +111,20 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email <span class="text-red-500">*</span></label>
|
||||||
<input type="email" id="email" th:field="*{email}"
|
<input type="email" id="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 focus:border-blue-500 outline-none transition-colors"
|
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
th:classappend="${#fields.hasErrors('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>
|
||||||
|
|
||||||
<!-- Résident Talange -->
|
<!-- Téléphone -->
|
||||||
<div class="flex items-center space-x-3 mt-7">
|
<div>
|
||||||
<input type="checkbox" id="residentTalange" th:field="*{residentTalange}" class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone <span class="text-red-500">*</span></label>
|
||||||
<label for="residentTalange" class="text-sm font-medium text-gray-700">
|
<input type="tel" id="telephone" th:field="*{telephone}" required
|
||||||
Résident de la commune de Talange
|
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"
|
||||||
</label>
|
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>
|
||||||
|
|
||||||
@@ -125,15 +148,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Adresse -->
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||||
<div>
|
<!-- Type de demande (Read-only) -->
|
||||||
<label for="adresse" class="block text-sm font-medium text-gray-700 mb-1">Adresse postale</label>
|
<div>
|
||||||
<textarea id="adresse" th:field="*{adresse}" rows="3"
|
<label for="typeDemande" class="block text-sm font-medium text-gray-700 mb-1">Type de demande (Pré-inscription)</label>
|
||||||
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"
|
<input type="text" id="typeDemande" th:field="*{typeDemande}" readonly
|
||||||
th:classappend="${#fields.hasErrors('adresse')} ? 'border-red-500' : ''"></textarea>
|
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">
|
||||||
<p th:if="${#fields.hasErrors('adresse')}" th:errors="*{adresse}" class="mt-1 text-xs text-red-600"></p>
|
</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>
|
||||||
|
|
||||||
|
<div id="ancienneCategorieBlock" class="grid grid-cols-1 md:grid-cols-2 gap-6 hidden-block mt-6">
|
||||||
|
<div>
|
||||||
|
<label for="ancienneCategorie" class="block text-sm font-medium text-gray-700 mb-1">Ancienne catégorie <span class="text-red-500">*</span></label>
|
||||||
|
<select id="ancienneCategorie" th:field="*{ancienneCategorie}"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
|
||||||
|
<option value="NA">NA (Je ne sais plus)</option>
|
||||||
|
<option value="U6">U6</option>
|
||||||
|
<option value="U7">U7</option>
|
||||||
|
<option value="U8">U8</option>
|
||||||
|
<option value="U9">U9</option>
|
||||||
|
<option value="U10">U10</option>
|
||||||
|
<option value="U11">U11</option>
|
||||||
|
<option value="U12">U12</option>
|
||||||
|
<option value="U13">U13</option>
|
||||||
|
<option value="U14">U14</option>
|
||||||
|
<option value="U15">U15</option>
|
||||||
|
<option value="U16">U16</option>
|
||||||
|
<option value="U17">U17</option>
|
||||||
|
<option value="Senior">Senior</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Removed Adresse -->
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||||
<a th:href="@{/adherents}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">Annuler</a>
|
<a th:href="@{/adherents}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">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 transition-colors">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 transition-colors">Enregistrer</button>
|
||||||
@@ -142,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">
|
||||||
@@ -150,11 +207,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="w-full text-left border-collapse">
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse min-w-[800px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||||
<th class="py-3 px-4 font-medium">Saison</th>
|
<th class="py-3 px-4 font-medium">Saison</th>
|
||||||
<th class="py-3 px-4 font-medium">Catégorie</th>
|
<th class="py-3 px-4 font-medium">Catégorie</th>
|
||||||
|
<th class="py-3 px-4 font-medium">Demande / Type</th>
|
||||||
<th class="py-3 px-4 font-medium">N° Licence</th>
|
<th class="py-3 px-4 font-medium">N° Licence</th>
|
||||||
<th class="py-3 px-4 font-medium">État</th>
|
<th class="py-3 px-4 font-medium">État</th>
|
||||||
<th class="py-3 px-4 font-medium">Tarif Global</th>
|
<th class="py-3 px-4 font-medium">Tarif Global</th>
|
||||||
@@ -164,7 +223,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(licences)}">
|
<tr th:if="${#lists.isEmpty(licences)}">
|
||||||
<td colspan="7" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
|
<td colspan="8" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<th:block th:each="lic : ${licences}">
|
<th:block th:each="lic : ${licences}">
|
||||||
<tr class="hover:bg-gray-50 border-b border-gray-100 licence-row"
|
<tr class="hover:bg-gray-50 border-b border-gray-100 licence-row"
|
||||||
@@ -177,6 +236,10 @@
|
|||||||
<span th:text="${lic.categorie.nom + (lic.equipe != null ? ' (' + lic.equipe.nom + ')' : '')}">U15</span>
|
<span th:text="${lic.categorie.nom + (lic.equipe != null ? ' (' + lic.equipe.nom + ')' : '')}">U15</span>
|
||||||
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
|
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="py-4 px-4 text-gray-600 text-xs">
|
||||||
|
<div th:text="${lic.typeDemande != null ? lic.typeDemande : '-'}">Nouvelle</div>
|
||||||
|
<div class="text-[10px] text-gray-500" th:text="${lic.typeLicence != null ? lic.typeLicence : '-'}">Libre</div>
|
||||||
|
</td>
|
||||||
<td class="py-4 px-4 text-gray-600 font-mono text-xs" th:text="${lic.numeroLicence != null and !lic.numeroLicence.isEmpty() ? lic.numeroLicence : '-'}">-</td>
|
<td class="py-4 px-4 text-gray-600 font-mono text-xs" th:text="${lic.numeroLicence != null and !lic.numeroLicence.isEmpty() ? lic.numeroLicence : '-'}">-</td>
|
||||||
<td class="py-4 px-4">
|
<td class="py-4 px-4">
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||||
@@ -193,8 +256,10 @@
|
|||||||
th:data-categorie-id="${lic.categorie.id}"
|
th:data-categorie-id="${lic.categorie.id}"
|
||||||
th:data-numero-licence="${lic.numeroLicence}"
|
th:data-numero-licence="${lic.numeroLicence}"
|
||||||
th:data-etat="${lic.etat}"
|
th:data-etat="${lic.etat}"
|
||||||
|
th:data-type-demande="${lic.typeDemande}"
|
||||||
|
th:data-type-licence="${lic.typeLicence}"
|
||||||
th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}"
|
th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}"
|
||||||
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-equipe-id'))"
|
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-type-demande'), this.getAttribute('data-type-licence'), this.getAttribute('data-equipe-id'))"
|
||||||
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
|
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
|
||||||
<button th:if="${lic.getResteAPayer() > 0}"
|
<button th:if="${lic.getResteAPayer() > 0}"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -205,16 +270,67 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
|
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
|
||||||
<td colspan="7" class="py-2 px-4">
|
<td colspan="8" class="py-3 px-6">
|
||||||
<div class="text-xs text-gray-500 mb-1 font-medium uppercase tracking-wider">Historique des paiements</div>
|
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Historique des paiements</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="overflow-x-auto">
|
||||||
<span th:each="paiement : ${lic.paiements}" class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-white border border-gray-200 text-gray-700">
|
<table class="min-w-full divide-y divide-gray-200 bg-white rounded-lg border border-gray-200 shadow-sm text-xs">
|
||||||
<span th:text="${paiement.montant + ' €'}">50 €</span>
|
<thead>
|
||||||
<span class="mx-1 text-gray-300">|</span>
|
<tr class="bg-gray-50 text-gray-500 uppercase font-medium">
|
||||||
<span th:text="${paiement.modePaiement.nom}">Chèque</span>
|
<th class="py-2 px-3 text-left">Date</th>
|
||||||
<span class="mx-1 text-gray-300">|</span>
|
<th class="py-2 px-3 text-left">Mode</th>
|
||||||
<span th:text="${#temporals.format(paiement.datePaiement, 'dd/MM/yyyy')}">01/01/2026</span>
|
<th class="py-2 px-3 text-left">Infos</th>
|
||||||
</span>
|
<th class="py-2 px-3 text-left">Géré par</th>
|
||||||
|
<th class="py-2 px-3 text-right font-medium">Montant</th>
|
||||||
|
<th class="py-2 px-3 text-right font-medium pr-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
<tr th:each="paiement : ${lic.paiements}" class="hover:bg-gray-50">
|
||||||
|
<td class="py-2 px-3 text-gray-600 font-medium" th:text="${#temporals.format(paiement.datePaiement, 'dd/MM/yyyy')}">01/01/2026</td>
|
||||||
|
<td class="py-2 px-3 text-gray-600">
|
||||||
|
<span class="px-2 py-0.5 rounded bg-gray-100 text-gray-700 font-mono text-[10px]" th:text="${paiement.modePaiement.nom}">Chèque</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3 text-gray-500 text-xs">
|
||||||
|
<div th:if="${paiement.numeroCheque != null and !paiement.numeroCheque.isEmpty()}" class="text-[10px]">
|
||||||
|
<span class="font-semibold text-gray-600">N°:</span> <span th:text="${paiement.numeroCheque}">123</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${paiement.commentaire != null and !paiement.commentaire.isEmpty()}" class="text-[10px] italic mt-0.5 text-gray-400 truncate max-w-[120px]" th:title="${paiement.commentaire}" th:text="${paiement.commentaire}">
|
||||||
|
Commentaire
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3 text-gray-500 text-xs italic" th:text="${paiement.gestionnaire != null ? paiement.gestionnaire : '-'}">-</td>
|
||||||
|
<td class="py-2 px-3 text-right font-semibold text-green-600" th:text="${paiement.montant + ' €'}">50.00 €</td>
|
||||||
|
<td class="py-2 px-3 text-right pr-4">
|
||||||
|
<div class="flex justify-end items-center space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
th:data-paiement-id="${paiement.id}"
|
||||||
|
th:data-montant="${paiement.montant}"
|
||||||
|
th:data-date="${paiement.datePaiement}"
|
||||||
|
th:data-mode-id="${paiement.modePaiement.id}"
|
||||||
|
th:data-licence-id="${lic.id}"
|
||||||
|
th:data-max-amount="${lic.getResteAPayer().add(paiement.montant)}"
|
||||||
|
th:data-cheque="${paiement.numeroCheque}"
|
||||||
|
th:data-commentaire="${paiement.commentaire}"
|
||||||
|
onclick="openEditPaiementModal(this.getAttribute('data-paiement-id'), this.getAttribute('data-montant'), this.getAttribute('data-date'), this.getAttribute('data-mode-id'), this.getAttribute('data-licence-id'), this.getAttribute('data-max-amount'), this.getAttribute('data-cheque'), this.getAttribute('data-commentaire'))"
|
||||||
|
class="text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 p-1.5 rounded transition-colors inline-flex items-center"
|
||||||
|
title="Modifier">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<form th:action="@{/paiements/{id}/delete(id=${paiement.id})}" method="post" class="inline m-0" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce paiement ?');">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||||
|
<button type="submit" class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-1.5 rounded transition-colors inline-flex items-center" title="Supprimer">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -242,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.equipement.obligatoire ? '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.equipement.obligatoire ? '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>
|
||||||
@@ -268,25 +377,51 @@
|
|||||||
<!-- Form inputs for bulk updating -->
|
<!-- Form inputs for bulk updating -->
|
||||||
<div class="space-y-2 pt-2 border-t border-gray-100">
|
<div class="space-y-2 pt-2 border-t border-gray-100">
|
||||||
<!-- Choisi checkbox for optional equipment -->
|
<!-- Choisi checkbox for optional equipment -->
|
||||||
<div class="flex items-center mb-1.5 bg-blue-50/50 p-1.5 rounded border border-blue-100/50" th:if="${!dot.equipement.obligatoire}">
|
<div class="flex items-center mb-1.5 bg-blue-50/50 p-1.5 rounded border border-blue-100/50" th:if="${!dot.licence.categorie.isEquipementObligatoire(dot.equipement.id)}">
|
||||||
<label class="flex items-center space-x-1.5 cursor-pointer select-none">
|
<label class="flex items-center space-x-1.5 cursor-pointer select-none">
|
||||||
<input type="checkbox" th:name="'choisi_' + ${dot.id}" th:checked="${dot.choisi}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
<input type="checkbox" th:name="'choisi_' + ${dot.id}" th:checked="${dot.choisi}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||||
<span class="text-[10px] font-bold text-blue-800 uppercase tracking-wide" th:text="'Commander (+ ' + ${dot.equipement.prixContribution} + ' €)'">Commander (+ 30 €)</span>
|
<span class="text-[10px] font-bold text-blue-800 uppercase tracking-wide" th:text="'Commander (+ ' + ${dot.licence.categorie.getEquipementPrix(dot.equipement.id)} + ' €)'">Commander (+ 30 €)</span>
|
||||||
</label>
|
</label>
|
||||||
</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"
|
||||||
@@ -306,8 +441,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</th:block>
|
</th:block>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -326,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>
|
||||||
@@ -344,6 +480,27 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
|
||||||
<input id="numeroLicenceInput" type="text" name="numeroLicence" placeholder="Ex: FFF-123456" 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 id="numeroLicenceInput" type="text" name="numeroLicence" placeholder="Ex: FFF-123456" 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">Type de demande</label>
|
||||||
|
<select id="typeDemandeSelect" name="typeDemande" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
<option value="">Sélectionnez...</option>
|
||||||
|
<option value="Renouvellement">Renouvellement</option>
|
||||||
|
<option value="Nouvelle demande">Nouvelle demande</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Type de licence</label>
|
||||||
|
<select id="typeLicenceSelect" name="typeLicence" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
<option value="">Sélectionnez...</option>
|
||||||
|
<option value="Animateur">Animateur</option>
|
||||||
|
<option value="Arbitre">Arbitre</option>
|
||||||
|
<option value="Dirigeant">Dirigeant</option>
|
||||||
|
<option value="Educateur Fédéral">Educateur Fédéral</option>
|
||||||
|
<option value="Foot loisir">Foot loisir</option>
|
||||||
|
<option value="Libre">Libre</option>
|
||||||
|
<option value="Technique">Technique</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Saison</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Saison</label>
|
||||||
<input id="saisonInput" type="text" name="saison" value="Saison Actuelle" 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">
|
<input id="saisonInput" type="text" name="saison" value="Saison Actuelle" 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">
|
||||||
@@ -383,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>
|
||||||
@@ -397,8 +562,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Paiement Modal -->
|
||||||
|
<div id="editPaiementModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||||
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-xl bg-white">
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<h3 class="text-lg leading-6 font-semibold text-gray-900 mb-4">Modifier le Paiement</h3>
|
||||||
|
<form id="editPaiementForm" method="post" class="space-y-4 text-left">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||||
|
<input type="hidden" name="adherentId" th:value="${adherent.id}">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Montant (€)</label>
|
||||||
|
<input type="number" step="0.01" min="0.01" id="editPaiementMontant" name="montant" 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">Date</label>
|
||||||
|
<input type="date" id="editPaiementDate" name="datePaiement" 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">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" onchange="toggleChequeVisibility(this, 'editChequeBlock', 'editNumeroCheque')">
|
||||||
|
<option value="">Sélectionnez un mode...</option>
|
||||||
|
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:data-nom="${#strings.toLowerCase(mode.nom)}" th:text="${mode.nom}"></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="editChequeBlock" class="hidden">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro du chèque <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="editNumeroCheque" name="numeroCheque" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Commentaire (facultatif)</label>
|
||||||
|
<textarea id="editCommentaire" name="commentaire" rows="2" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="items-center px-4 py-3 flex justify-end space-x-2">
|
||||||
|
<button type="button" onclick="closeEditPaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', equipeId = '') {
|
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', typeDemande = '', typeLicence = '', equipeId = '') {
|
||||||
document.getElementById('licenceModal').classList.remove('hidden');
|
document.getElementById('licenceModal').classList.remove('hidden');
|
||||||
const form = document.getElementById('licenceForm');
|
const form = document.getElementById('licenceForm');
|
||||||
const title = document.getElementById('licenceModalTitle');
|
const title = document.getElementById('licenceModalTitle');
|
||||||
@@ -415,6 +620,8 @@
|
|||||||
catSelect.value = categorieId;
|
catSelect.value = categorieId;
|
||||||
document.getElementById('numeroLicenceInput').value = numeroLicence;
|
document.getElementById('numeroLicenceInput').value = numeroLicence;
|
||||||
document.getElementById('etatSelect').value = etat;
|
document.getElementById('etatSelect').value = etat;
|
||||||
|
document.getElementById('typeDemandeSelect').value = typeDemande;
|
||||||
|
document.getElementById('typeLicenceSelect').value = typeLicence;
|
||||||
|
|
||||||
// Fetch and populate teams
|
// Fetch and populate teams
|
||||||
if (categorieId) {
|
if (categorieId) {
|
||||||
@@ -431,25 +638,54 @@
|
|||||||
document.getElementById('numeroLicenceInput').value = '';
|
document.getElementById('numeroLicenceInput').value = '';
|
||||||
document.getElementById('etatSelect').value = 'Brouillon';
|
document.getElementById('etatSelect').value = 'Brouillon';
|
||||||
|
|
||||||
// Pré-sélection de la catégorie selon l'année de naissance
|
// Pré-remplissage du type de demande
|
||||||
|
const adherentTypeDemande = document.getElementById('typeDemande') ? document.getElementById('typeDemande').value : '';
|
||||||
|
const typeDemandeSelect = document.getElementById('typeDemandeSelect');
|
||||||
|
if (adherentTypeDemande === 'NOUVELLE') {
|
||||||
|
typeDemandeSelect.value = 'Nouvelle demande';
|
||||||
|
} else if (adherentTypeDemande === 'RENOUVELLEMENT') {
|
||||||
|
typeDemandeSelect.value = 'Renouvellement';
|
||||||
|
} else {
|
||||||
|
typeDemandeSelect.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('typeLicenceSelect').value = '';
|
||||||
|
|
||||||
|
// Pré-sélection de la catégorie selon l'année de naissance 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 = '';
|
||||||
}
|
}
|
||||||
@@ -478,6 +714,36 @@
|
|||||||
function closePaiementModal() {
|
function closePaiementModal() {
|
||||||
document.getElementById('paiementModal').classList.add('hidden');
|
document.getElementById('paiementModal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditPaiementModal(paiementId, montant, date, modePaiementId, licenceId, maxAmount, numeroCheque, commentaire) {
|
||||||
|
document.getElementById('editPaiementModal').classList.remove('hidden');
|
||||||
|
document.getElementById('editPaiementForm').action = '/paiements/' + paiementId + '/update';
|
||||||
|
document.getElementById('editPaiementMontant').value = montant;
|
||||||
|
document.getElementById('editPaiementMontant').max = maxAmount;
|
||||||
|
document.getElementById('editPaiementDate').value = date;
|
||||||
|
document.getElementById('editPaiementMode').value = modePaiementId;
|
||||||
|
document.getElementById('editNumeroCheque').value = numeroCheque || '';
|
||||||
|
document.getElementById('editCommentaire').value = commentaire || '';
|
||||||
|
toggleChequeVisibility(document.getElementById('editPaiementMode'), 'editChequeBlock', 'editNumeroCheque');
|
||||||
|
}
|
||||||
|
function closeEditPaiementModal() {
|
||||||
|
document.getElementById('editPaiementModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleChequeVisibility(selectElement, blockId, inputId) {
|
||||||
|
const block = document.getElementById(blockId);
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const selectedOption = selectElement.options[selectElement.selectedIndex];
|
||||||
|
|
||||||
|
if (selectedOption && selectedOption.getAttribute('data-nom') && selectedOption.getAttribute('data-nom').includes('chèque')) {
|
||||||
|
block.classList.remove('hidden');
|
||||||
|
input.required = true;
|
||||||
|
} else {
|
||||||
|
block.classList.add('hidden');
|
||||||
|
input.required = false;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const dateInput = document.getElementById('dateNaissance');
|
const dateInput = document.getElementById('dateNaissance');
|
||||||
const repBlock = document.getElementById('representantBlock');
|
const repBlock = document.getElementById('representantBlock');
|
||||||
@@ -519,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');
|
||||||
@@ -533,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 => {
|
||||||
@@ -691,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);
|
||||||
|
|||||||
@@ -34,7 +34,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="adherents-table-container" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<!-- Table Container with Wrapper-level HTMX triggers for filters -->
|
||||||
|
<div id="adherents-table-container"
|
||||||
|
hx-get="/adherents"
|
||||||
|
hx-target="this"
|
||||||
|
hx-select="#adherents-table-container"
|
||||||
|
hx-trigger="change from:#filter-row, keyup changed delay:300ms from:#filter-row"
|
||||||
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||||
|
hx-sync="this:replace"
|
||||||
|
hx-on::config-request="if (event.detail.trigger.tagName !== 'BUTTON') { event.detail.parameters['page'] = 0; }"
|
||||||
|
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
|
||||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
<form hx-get="/adherents" hx-target="#adherents-table-container" hx-select="#adherents-table-container" class="flex items-center space-x-2">
|
<form hx-get="/adherents" hx-target="#adherents-table-container" hx-select="#adherents-table-container" class="flex items-center space-x-2">
|
||||||
<input type="text" name="query" th:value="${searchQuery}" placeholder="Nom, Prénom ou N° Licence..."
|
<input type="text" name="query" th:value="${searchQuery}" placeholder="Nom, Prénom ou N° Licence..."
|
||||||
@@ -44,90 +54,108 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="filter-form" hx-get="/adherents" hx-target="#adherents-table-container" hx-select="#adherents-table-container" hx-trigger="change, keyup changed delay:300ms" hx-sync="this:replace" hx-on::config-request="if (event.detail.trigger.tagName !== 'BUTTON') { event.detail.parameters['page'] = 0; }" class="m-0">
|
<div class="overflow-x-auto">
|
||||||
<input type="hidden" name="query" th:value="${searchQuery}">
|
<table class="w-full text-left border-collapse min-w-[1000px]">
|
||||||
|
<thead>
|
||||||
<table class="w-full text-left border-collapse">
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||||
<thead>
|
<th class="py-3 px-6 font-medium text-left">Nom</th>
|
||||||
<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">Prénom</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Nom</th>
|
<th class="py-3 px-6 font-medium text-left">Catégorie</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Prénom</th>
|
<th class="py-3 px-6 font-medium text-center">N° Licence</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Catégorie</th>
|
<th class="py-3 px-6 font-medium text-left">Email</th>
|
||||||
<th class="py-3 px-6 font-medium text-center">N° Licence</th>
|
<th class="py-3 px-6 font-medium text-center">Paiement</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Email</th>
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Téléphone</th>
|
</tr>
|
||||||
<th class="py-3 px-6 font-medium text-center">Paiement</th>
|
<!-- Filter Row -->
|
||||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
<tr id="filter-row" class="bg-gray-100 border-b border-gray-200 text-xs">
|
||||||
</tr>
|
<td class="py-2 px-3">
|
||||||
<!-- Filter Row -->
|
<input type="text" id="filterNom" name="nom" th:value="${nomFilter}" placeholder="Filtrer Nom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
||||||
<tr class="bg-gray-100 border-b border-gray-200 text-xs">
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<input type="text" id="filterNom" name="nom" th:value="${nomFilter}" placeholder="Filtrer Nom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
<input type="text" id="filterPrenom" name="prenom" th:value="${prenomFilter}" placeholder="Filtrer Prénom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<input type="text" id="filterPrenom" name="prenom" th:value="${prenomFilter}" placeholder="Filtrer Prénom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
<select id="filterCategorie" name="categorie" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
||||||
</td>
|
<option value="">Toutes</option>
|
||||||
<td class="py-2 px-3">
|
<option th:each="cat : ${categories}" th:value="${cat.nom}" th:text="${cat.nom}" th:selected="${categorieFilter == cat.nom}"></option>
|
||||||
<select id="filterCategorie" name="categorie" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
</select>
|
||||||
<option value="">Toutes</option>
|
</td>
|
||||||
<option th:each="cat : ${categories}" th:value="${cat.nom}" th:text="${cat.nom}" th:selected="${categorieFilter == cat.nom}"></option>
|
<td class="py-2 px-3">
|
||||||
</select>
|
<input type="text" id="filterLicence" name="licence" th:value="${licenceFilter}" placeholder="Filtrer N°..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<input type="text" id="filterLicence" name="licence" th:value="${licenceFilter}" placeholder="Filtrer N°..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
<input type="text" id="filterEmail" name="email" th:value="${emailFilter}" placeholder="Filtrer Email..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<input type="text" id="filterEmail" name="email" th:value="${emailFilter}" placeholder="Filtrer Email..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
<select id="filterPaiement" name="paiement" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
||||||
</td>
|
<option value="">Tous</option>
|
||||||
<td class="py-2 px-3">
|
<option value="A_JOUR" th:selected="${paiementFilter == 'A_JOUR'}">À jour</option>
|
||||||
<input type="text" id="filterTelephone" name="telephone" th:value="${telephoneFilter}" placeholder="Filtrer Tél..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
<option value="RESTE" th:selected="${paiementFilter == 'RESTE'}">Reste à payer</option>
|
||||||
</td>
|
<option value="AUCUNE" th:selected="${paiementFilter == 'AUCUNE'}">Aucune licence</option>
|
||||||
<td class="py-2 px-3">
|
</select>
|
||||||
<select id="filterPaiement" name="paiement" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
</td>
|
||||||
<option value="">Tous</option>
|
<td class="py-2 px-3"></td>
|
||||||
<option value="A_JOUR" th:selected="${paiementFilter == 'A_JOUR'}">À jour</option>
|
</tr>
|
||||||
<option value="RESTE" th:selected="${paiementFilter == 'RESTE'}">Reste à payer</option>
|
</thead>
|
||||||
<option value="AUCUNE" th:selected="${paiementFilter == 'AUCUNE'}">Aucune licence</option>
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
</select>
|
<tr th:if="${#lists.isEmpty(adherents)}">
|
||||||
</td>
|
<td colspan="7" class="py-8 text-center text-gray-500">Aucun adhérent enregistré.</td>
|
||||||
<td class="py-2 px-3"></td>
|
</tr>
|
||||||
</tr>
|
<tr th:each="adherent : ${adherents}" class="hover:bg-gray-50 transition-colors adherent-row">
|
||||||
</thead>
|
<td class="py-4 px-6 font-medium text-gray-900">
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<span th:text="${adherent.nom}">Dupont</span>
|
||||||
<tr th:if="${#lists.isEmpty(adherents)}">
|
<div class="text-[10px] text-gray-400 font-normal mt-0.5">
|
||||||
<td colspan="8" class="py-8 text-center text-gray-500">Aucun adhérent enregistré.</td>
|
<span th:text="${adherent.sexe == 'FEMININ' ? 'Féminin' : 'Masculin'}">Masculin</span>
|
||||||
</tr>
|
<span class="mx-1">|</span>
|
||||||
<tr th:each="adherent : ${adherents}" class="hover:bg-gray-50 transition-colors adherent-row">
|
<span th:text="${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}">Joueur</span>
|
||||||
<td class="py-4 px-6 font-medium text-gray-900">
|
</div>
|
||||||
<span th:text="${adherent.nom}">Dupont</span>
|
</td>
|
||||||
<div class="text-[10px] text-gray-400 font-normal mt-0.5">
|
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.prenom}">Jean</td>
|
||||||
<span th:text="${adherent.sexe == 'FEMININ' ? 'Féminin' : 'Masculin'}">Masculin</span>
|
<td class="py-4 px-6 text-gray-600 font-medium"
|
||||||
<span class="mx-1">|</span>
|
th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() + (adherent.getLicenceActuelle().getEquipe() != null ? ' (' + adherent.getLicenceActuelle().getEquipe().getNom() + ')' : '') : '-'}">-</td>
|
||||||
<span th:text="${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}">Joueur</span>
|
<td class="py-4 px-6 text-center text-gray-600 font-mono text-xs"
|
||||||
|
th:text="${adherent.getLicenceActuelle() != null and adherent.getLicenceActuelle().numeroLicence != null and !adherent.getLicenceActuelle().numeroLicence.isEmpty() ? adherent.getLicenceActuelle().numeroLicence : '-'}">-</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600" th:text="${adherent.email}">jean@example.com</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<!-- Aucune licence -->
|
||||||
|
<div th:if="${adherent.getLicenceActuelle() == null}" class="text-center text-gray-400 text-xs italic">
|
||||||
|
Aucune licence
|
||||||
|
</div>
|
||||||
|
<!-- Détails financiers -->
|
||||||
|
<div th:if="${adherent.getLicenceActuelle() != null}" class="flex flex-col items-center space-y-1">
|
||||||
|
<!-- Badge Statut -->
|
||||||
|
<span class="px-2 py-0.5 text-[9px] font-bold rounded-full uppercase tracking-wider"
|
||||||
|
th:classappend="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}"
|
||||||
|
th:text="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'À jour' : 'Reste à payer'}">
|
||||||
|
À jour
|
||||||
|
</span>
|
||||||
|
<!-- Tableau/Liste miniature des montants -->
|
||||||
|
<div class="text-[11px] font-mono text-gray-600 bg-gray-50 p-1.5 rounded-lg border border-gray-100 w-28 space-y-0.5 shadow-2xs mx-auto">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-400">Dû:</span>
|
||||||
|
<span class="font-semibold text-gray-800" th:text="|${adherent.getLicenceActuelle().getPrixTotal()} €|">150 €</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-400">Payé:</span>
|
||||||
|
<span class="font-semibold text-green-600" th:text="|${adherent.getLicenceActuelle().getSommePayee()} €|">100 €</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between border-t border-gray-200 pt-0.5 mt-0.5">
|
||||||
|
<span class="text-gray-500 font-medium">Reste:</span>
|
||||||
|
<span class="font-bold" th:classappend="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'text-gray-800' : 'text-red-600'}" th:text="|${adherent.getLicenceActuelle().getResteAPayer()} €|">50 €</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.prenom}">Jean</td>
|
</td>
|
||||||
<td class="py-4 px-6 text-gray-600 font-medium"
|
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||||
th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() + (adherent.getLicenceActuelle().getEquipe() != null ? ' (' + adherent.getLicenceActuelle().getEquipe().getNom() + ')' : '') : '-'}">-</td>
|
<a th:href="@{/adherents/{id}/edit(id=${adherent.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Voir / Modifier</a>
|
||||||
<td class="py-4 px-6 text-center text-gray-600 font-mono text-xs"
|
<form th:action="@{/adherents/{id}/delete(id=${adherent.id})}" method="post" onsubmit="return confirm('Supprimer cet adhérent ?');">
|
||||||
th:text="${adherent.getLicenceActuelle() != null and adherent.getLicenceActuelle().numeroLicence != null and !adherent.getLicenceActuelle().numeroLicence.isEmpty() ? adherent.getLicenceActuelle().numeroLicence : '-'}">-</td>
|
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${adherent.email}">jean@example.com</td>
|
</form>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${adherent.telephone}">0600000000</td>
|
</td>
|
||||||
<td class="py-4 px-6 text-center">
|
</tr>
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
</tbody>
|
||||||
th:classappend="${adherent.getStatutPaiement() == 'À jour' ? 'bg-green-100 text-green-800' : (adherent.getStatutPaiement() == 'Aucune licence' ? 'bg-gray-100 text-gray-800' : 'bg-red-100 text-red-800')}"
|
|
||||||
th:text="${adherent.getStatutPaiement()}">À jour</span>
|
|
||||||
</td>
|
|
||||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
|
||||||
<a th:href="@{/adherents/{id}/edit(id=${adherent.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Voir / Modifier</a>
|
|
||||||
<form th:action="@{/adherents/{id}/delete(id=${adherent.id})}" method="post" onsubmit="return confirm('Supprimer cet adhérent ?');">
|
|
||||||
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Footer -->
|
<!-- Pagination Footer -->
|
||||||
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between bg-gray-50 text-sm text-gray-700">
|
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between bg-gray-50 text-sm text-gray-700">
|
||||||
@@ -146,8 +174,8 @@
|
|||||||
hx-get="/adherents"
|
hx-get="/adherents"
|
||||||
hx-target="#adherents-table-container"
|
hx-target="#adherents-table-container"
|
||||||
hx-select="#adherents-table-container"
|
hx-select="#adherents-table-container"
|
||||||
hx-include="#filter-form"
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||||
th:attr="hx-vals=|{'page': ${currentPage - 1}}|"
|
th:attr="hx-vals=|{"page": ${currentPage - 1}}|"
|
||||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||||
Précédent
|
Précédent
|
||||||
</button>
|
</button>
|
||||||
@@ -161,8 +189,8 @@
|
|||||||
hx-get="/adherents"
|
hx-get="/adherents"
|
||||||
hx-target="#adherents-table-container"
|
hx-target="#adherents-table-container"
|
||||||
hx-select="#adherents-table-container"
|
hx-select="#adherents-table-container"
|
||||||
hx-include="#filter-form"
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||||
th:attr="hx-vals=|{'page': ${pageNum}}|"
|
th:attr="hx-vals=|{"page": ${pageNum}}|"
|
||||||
th:text="${pageNum + 1}"
|
th:text="${pageNum + 1}"
|
||||||
class="px-3 py-1 rounded transition-colors"
|
class="px-3 py-1 rounded transition-colors"
|
||||||
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
|
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
|
||||||
@@ -176,8 +204,8 @@
|
|||||||
hx-get="/adherents"
|
hx-get="/adherents"
|
||||||
hx-target="#adherents-table-container"
|
hx-target="#adherents-table-container"
|
||||||
hx-select="#adherents-table-container"
|
hx-select="#adherents-table-container"
|
||||||
hx-include="#filter-form"
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
||||||
th:attr="hx-vals=|{'page': ${currentPage + 1}}|"
|
th:attr="hx-vals=|{"page": ${currentPage + 1}}|"
|
||||||
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
||||||
Suivant
|
Suivant
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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,41 +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">
|
||||||
|
<span>Pré-inscriptions</span>
|
||||||
|
<span hx-get="/admin/pre-inscriptions/count" hx-trigger="load, every 30s, refreshBadge from:body" hx-swap="outerHTML"></span>
|
||||||
|
</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="/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>
|
||||||
@@ -79,18 +89,18 @@
|
|||||||
<h3 class="text-lg font-semibold text-gray-900">Derniers adhérents inscrits</h3>
|
<h3 class="text-lg font-semibold text-gray-900">Derniers adhérents inscrits</h3>
|
||||||
<a href="/adherents" class="text-sm font-medium text-blue-600 hover:text-blue-800">Voir tout →</a>
|
<a href="/adherents" class="text-sm font-medium text-blue-600 hover:text-blue-800">Voir tout →</a>
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-left border-collapse">
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse min-w-[600px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-50/50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
|
<tr class="bg-gray-50/50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
|
||||||
<th class="py-3 px-6 font-medium">Nom & Prénom</th>
|
<th class="py-3 px-6 font-medium">Nom & Prénom</th>
|
||||||
<th class="py-3 px-6 font-medium">Date de naissance</th>
|
<th class="py-3 px-6 font-medium">Date de naissance</th>
|
||||||
<th class="py-3 px-6 font-medium">Téléphone</th>
|
|
||||||
<th class="py-3 px-6 font-medium text-right">Action</th>
|
<th class="py-3 px-6 font-medium text-right">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-100 text-sm">
|
<tbody class="divide-y divide-gray-100 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(derniersInscrits)}">
|
<tr th:if="${#lists.isEmpty(derniersInscrits)}">
|
||||||
<td colspan="4" class="py-4 px-6 text-center text-gray-500">Aucun adhérent récent.</td>
|
<td colspan="3" class="py-4 px-6 text-center text-gray-500">Aucun adhérent récent.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="adh : ${derniersInscrits}" class="hover:bg-gray-50 transition-colors">
|
<tr th:each="adh : ${derniersInscrits}" class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="py-4 px-6">
|
<td class="py-4 px-6">
|
||||||
@@ -98,13 +108,13 @@
|
|||||||
<div class="text-xs text-gray-500" th:text="${adh.email}">jean@example.com</div>
|
<div class="text-xs text-gray-500" th:text="${adh.email}">jean@example.com</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${#temporals.format(adh.dateNaissance, 'dd/MM/yyyy')}">01/01/2000</td>
|
<td class="py-4 px-6 text-gray-600" th:text="${#temporals.format(adh.dateNaissance, 'dd/MM/yyyy')}">01/01/2000</td>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${adh.telephone}">0600000000</td>
|
|
||||||
<td class="py-4 px-6 text-right">
|
<td class="py-4 px-6 text-right">
|
||||||
<a th:href="@{/adherents/{id}/edit(id=${adh.id})}" class="text-blue-600 hover:text-blue-800 font-medium">Voir profil</a>
|
<a th:href="@{/adherents/{id}/edit(id=${adh.id})}" class="text-blue-600 hover:text-blue-800 font-medium">Voir profil</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
|
|
||||||
<form id="filter-form" hx-get="/paiements" hx-target="#paiements-table-container" hx-select="#paiements-table-container" hx-trigger="change, keyup changed delay:300ms" hx-sync="this:replace" hx-on::config-request="if (event.detail.trigger.tagName !== 'BUTTON') { event.detail.parameters['page'] = 0; }" class="m-0">
|
<form id="filter-form" hx-get="/paiements" hx-target="#paiements-table-container" hx-select="#paiements-table-container" hx-trigger="change, keyup changed delay:300ms" hx-sync="this:replace" hx-on::config-request="if (event.detail.trigger.tagName !== 'BUTTON') { event.detail.parameters['page'] = 0; }" class="m-0">
|
||||||
<input type="hidden" name="query" th:value="${searchQuery}">
|
<input type="hidden" name="query" th:value="${searchQuery}">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left border-collapse min-w-[1000px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||||
<th class="py-3 px-6 font-medium text-left">Date</th>
|
<th class="py-3 px-6 font-medium text-left">Date</th>
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
<th class="py-3 px-6 font-medium text-center">Saison</th>
|
<th class="py-3 px-6 font-medium text-center">Saison</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Mode de paiement</th>
|
<th class="py-3 px-6 font-medium text-left">Mode de paiement</th>
|
||||||
<th class="py-3 px-6 font-medium text-right">Montant</th>
|
<th class="py-3 px-6 font-medium text-right">Montant</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-right pr-6">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Filter Row -->
|
<!-- Filter Row -->
|
||||||
<tr class="bg-gray-100 border-b border-gray-200 text-xs">
|
<tr class="bg-gray-100 border-b border-gray-200 text-xs">
|
||||||
@@ -69,11 +70,12 @@
|
|||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<input type="text" id="filterMontant" name="montant" th:value="${montantFilter}" placeholder="Filtrer Montant..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none text-right">
|
<input type="text" id="filterMontant" name="montant" th:value="${montantFilter}" placeholder="Filtrer Montant..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none text-right">
|
||||||
</td>
|
</td>
|
||||||
|
<td class="py-2 px-3"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(paiements)}">
|
<tr th:if="${#lists.isEmpty(paiements)}">
|
||||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucun paiement trouvé.</td>
|
<td colspan="7" class="py-8 text-center text-gray-500">Aucun paiement trouvé.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="p : ${paiements}" class="hover:bg-gray-50 transition-colors">
|
<tr th:each="p : ${paiements}" class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${#temporals.format(p.datePaiement, 'dd/MM/yyyy')}">15/05/2026</td>
|
<td class="py-4 px-6 text-gray-600" th:text="${#temporals.format(p.datePaiement, 'dd/MM/yyyy')}">15/05/2026</td>
|
||||||
@@ -89,9 +91,37 @@
|
|||||||
th:text="${p.modePaiement.nom}">Chèque</span>
|
th:text="${p.modePaiement.nom}">Chèque</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 text-right font-semibold text-green-600" th:text="${p.montant + ' €'}">50.00 €</td>
|
<td class="py-4 px-6 text-right font-semibold text-green-600" th:text="${p.montant + ' €'}">50.00 €</td>
|
||||||
|
<td class="py-4 px-6 text-right pr-6">
|
||||||
|
<div class="flex justify-end items-center space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
th:data-paiement-id="${p.id}"
|
||||||
|
th:data-montant="${p.montant}"
|
||||||
|
th:data-date="${p.datePaiement}"
|
||||||
|
th:data-mode-id="${p.modePaiement.id}"
|
||||||
|
th:data-licence-id="${p.licence.id}"
|
||||||
|
th:data-max-amount="${p.licence.getResteAPayer().add(p.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'))"
|
||||||
|
class="text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 p-1.5 rounded transition-colors inline-flex items-center"
|
||||||
|
title="Modifier">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<form th:action="@{/paiements/{id}/delete(id=${p.id})}" method="post" class="inline m-0" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce paiement ?');">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||||
|
<input type="hidden" name="redirect" value="/paiements" />
|
||||||
|
<button type="submit" class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-1.5 rounded transition-colors inline-flex items-center" title="Supprimer">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Pagination Footer -->
|
<!-- Pagination Footer -->
|
||||||
@@ -154,5 +184,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Edit Paiement Modal -->
|
||||||
|
<div id="editPaiementModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||||
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-xl bg-white text-left">
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<h3 class="text-lg leading-6 font-semibold text-gray-900 mb-4">Modifier le Paiement</h3>
|
||||||
|
<form id="editPaiementForm" method="post" class="space-y-4 text-left">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||||
|
<input type="hidden" name="redirect" value="/paiements">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Montant (€)</label>
|
||||||
|
<input type="number" step="0.01" min="0.01" id="editPaiementMontant" name="montant" 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">Date</label>
|
||||||
|
<input type="date" id="editPaiementDate" name="datePaiement" 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">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">
|
||||||
|
<option value="">Sélectionnez un mode...</option>
|
||||||
|
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="items-center px-4 py-3 flex justify-end space-x-2">
|
||||||
|
<button type="button" onclick="closeEditPaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openEditPaiementModal(paiementId, montant, date, modePaiementId, licenceId, maxAmount) {
|
||||||
|
document.getElementById('editPaiementModal').classList.remove('hidden');
|
||||||
|
document.getElementById('editPaiementForm').action = '/paiements/' + paiementId + '/update';
|
||||||
|
document.getElementById('editPaiementMontant').value = montant;
|
||||||
|
document.getElementById('editPaiementMontant').max = maxAmount;
|
||||||
|
document.getElementById('editPaiementDate').value = date;
|
||||||
|
document.getElementById('editPaiementMode').value = modePaiementId;
|
||||||
|
}
|
||||||
|
function closeEditPaiementModal() {
|
||||||
|
document.getElementById('editPaiementModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
</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>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">
|
||||||
@@ -50,6 +60,97 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 border-t border-gray-100">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Équipements liés à la catégorie</label>
|
||||||
|
|
||||||
|
<div class="flex space-x-2 mb-4">
|
||||||
|
<select id="equipementToAdd" class="flex-1 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
<option value="">Sélectionner un équipement à ajouter...</option>
|
||||||
|
<th:block th:each="equi : ${allEquipements}">
|
||||||
|
<option th:value="${equi.id}" th:text="${equi.nom}"></option>
|
||||||
|
</th:block>
|
||||||
|
</select>
|
||||||
|
<button type="button" onclick="addEquipement()" class="px-4 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">Ajouter</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="equipementsContainer" class="space-y-3 max-h-64 overflow-y-auto">
|
||||||
|
<th:block th:each="equi : ${allEquipements}" th:if="${categorie.hasEquipement(equi.id)}">
|
||||||
|
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row" th:id="'eq-row-' + ${equi.id}">
|
||||||
|
<div class="w-1/4">
|
||||||
|
<span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/3">
|
||||||
|
<select th:name="'etatEquipement_' + ${equi.id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
<option value="OBLIGATOIRE" th:selected="${categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option>
|
||||||
|
<option value="OPTIONNEL" th:selected="${!categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/4 flex items-center space-x-2">
|
||||||
|
<label class="text-xs text-gray-500">Prix (€)</label>
|
||||||
|
<input type="number" step="0.01" th:name="'prixEquipement_' + ${equi.id}"
|
||||||
|
th:value="${categorie.getEquipementPrix(equi.id)}"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="w-auto">
|
||||||
|
<button type="button" th:onclick="'removeEquipement(' + ${equi.id} + ')'" class="text-red-600 hover:text-red-800 text-sm font-medium">Retirer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function addEquipement() {
|
||||||
|
const select = document.getElementById('equipementToAdd');
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
|
||||||
|
if (!selectedOption.value) return;
|
||||||
|
|
||||||
|
const id = selectedOption.value;
|
||||||
|
const nom = selectedOption.text;
|
||||||
|
|
||||||
|
if (document.getElementById('eq-row-' + id)) {
|
||||||
|
alert("Cet équipement est déjà lié à la catégorie.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById('equipementsContainer');
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100 equipement-row';
|
||||||
|
row.id = 'eq-row-' + id;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<div class="w-1/4">
|
||||||
|
<span class="block text-sm font-medium text-gray-900">${nom}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/3">
|
||||||
|
<select name="etatEquipement_${id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
<option value="OBLIGATOIRE">Obligatoire (inclus)</option>
|
||||||
|
<option value="OPTIONNEL">Optionnel (payant)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/4 flex items-center space-x-2">
|
||||||
|
<label class="text-xs text-gray-500">Prix (€)</label>
|
||||||
|
<input type="number" step="0.01" name="prixEquipement_${id}" value="0.00" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="w-auto">
|
||||||
|
<button type="button" onclick="removeEquipement(${id})" class="text-red-600 hover:text-red-800 text-sm font-medium">Retirer</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(row);
|
||||||
|
select.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEquipement(id) {
|
||||||
|
const row = document.getElementById('eq-row-' + id);
|
||||||
|
if (row) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
<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,13 +20,17 @@
|
|||||||
<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">
|
||||||
<table class="w-full text-left border-collapse">
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse min-w-[800px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||||
<th class="py-3 px-6 font-medium text-left">Nom de la catégorie</th>
|
<th class="py-3 px-6 font-medium text-left">Nom de la catégorie</th>
|
||||||
@@ -41,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>
|
||||||
@@ -54,9 +62,20 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</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,72 @@
|
|||||||
|
<!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>Éducateur - 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 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" th:text="${educateur.id == null ? 'Nouvel Éducateur' : 'Modifier Éducateur'}">Éducateur</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-auto p-6">
|
||||||
|
<div class="max-w-xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||||
|
<form th:action="@{/educateurs}" th:object="${educateur}" method="post" class="space-y-6">
|
||||||
|
<input type="hidden" th:field="*{id}" />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Nom <span class="text-red-500">*</span></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 font-semibold">Prénom <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" th:field="*{prenom}" 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 font-semibold">Numéro de Téléphone (non obligatoire)</label>
|
||||||
|
<input type="tel" th:field="*{telephone}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Adresse E-mail (facultatif)</label>
|
||||||
|
<input type="email" th:field="*{email}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Catégorie</label>
|
||||||
|
<select th:field="*{categorie}" 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="">-- Aucune --</option>
|
||||||
|
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom}">U15</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1 font-semibold">Équipe</label>
|
||||||
|
<select th:field="*{equipe}" 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="">-- Aucune --</option>
|
||||||
|
<option th:each="equip : ${equipes}" th:value="${equip.id}" th:text="${equip.nom + ' (' + equip.categorie.nom + ')'}">Équipe A (U15)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||||
|
<a th:href="@{/educateurs}" 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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<!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 - Éducateurs</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">Éducateurs / Entraîneurs</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-auto p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-xl font-bold text-gray-900">Liste des Éducateurs</h3>
|
||||||
|
<a th:href="@{/educateurs/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||||
|
+ Nouvel Éducateur
|
||||||
|
</a>
|
||||||
|
</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</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Prénom</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Téléphone</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Email</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Catégorie</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-left">Équipe</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
|
<tr th:if="${#lists.isEmpty(educateurs)}">
|
||||||
|
<td colspan="7" class="py-8 text-center text-gray-500">Aucun éducateur n'est paramétré.</td>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="ed : ${educateurs}" class="hover:bg-gray-50 transition-colors">
|
||||||
|
<td class="py-4 px-6 font-medium text-gray-900" th:text="${ed.nom}">Dupont</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600" th:text="${ed.prenom}">Jean</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600" th:text="${ed.telephone != null and !ed.telephone.isEmpty() ? ed.telephone : '-'}">-</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600" th:text="${ed.email != null and !ed.email.isEmpty() ? ed.email : '-'}">-</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600">
|
||||||
|
<span th:if="${ed.categorie != null}" th:text="${ed.categorie.nom}" class="px-2.5 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">U15</span>
|
||||||
|
<span th:if="${ed.categorie == null}">-</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-gray-600">
|
||||||
|
<span th:if="${ed.equipe != null}" th:text="${ed.equipe.nom}" class="px-2.5 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800">Équipe A</span>
|
||||||
|
<span th:if="${ed.equipe == null}">-</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||||
|
<a th:href="@{/educateurs/{id}/edit(id=${ed.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
||||||
|
<form th:action="@{/educateurs/{id}/delete(id=${ed.id})}" method="post" onsubmit="return confirm('Supprimer cet éducateur ?');">
|
||||||
|
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
||||||
|
</form>
|
||||||
|
</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>Équipement - AS Talange</title>
|
<title>Équipement - AS Talange</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
@@ -27,23 +28,45 @@
|
|||||||
</div>
|
</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">Référence</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>
|
<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>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Prix de contribution (€) - Pour équipement facultatif</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Description (ex: Maillot domicile officiel)</label>
|
||||||
<input type="number" step="0.01" th:field="*{prixContribution}" 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">
|
<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>
|
||||||
<p class="text-xs text-gray-500 mt-1">Si l'équipement est facultatif, ce montant sera ajouté au tarif global de la licence s'il est commandé.</p>
|
|
||||||
</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="flex items-center space-x-3">
|
<div class="space-y-4">
|
||||||
<input type="checkbox" id="obligatoire" th:field="*{obligatoire}" class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
<div class="flex items-center">
|
||||||
<label for="obligatoire" class="text-sm font-medium text-gray-700">
|
<input type="checkbox" th:field="*{gestionSexe}" class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||||
Cet équipement est obligatoire pour tous les licenciés (inclus dans la licence)
|
<label class="ml-2 block text-sm text-gray-700">Gérer les coupes Homme/Femme</label>
|
||||||
</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>
|
||||||
|
|
||||||
<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,35 +20,28 @@
|
|||||||
<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">
|
||||||
<th class="py-3 px-6 font-medium text-left">Nom de l'équipement</th>
|
<th class="py-3 px-6 font-medium text-left">Nom de l'équipement</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Description</th>
|
<th class="py-3 px-6 font-medium text-left">Description</th>
|
||||||
<th class="py-3 px-6 font-medium text-center">Obligatoire</th>
|
|
||||||
<th class="py-3 px-6 font-medium text-center">Prix Contribution</th>
|
|
||||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(equipements)}">
|
<tr th:if="${#lists.isEmpty(equipements)}">
|
||||||
<td colspan="5" class="py-8 text-center text-gray-500">Aucun équipement n'est paramétré.</td>
|
<td colspan="3" class="py-8 text-center text-gray-500">Aucun équipement n'est paramétré.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="equip : ${equipements}" class="hover:bg-gray-50 transition-colors">
|
<tr th:each="equip : ${equipements}" class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${equip.nom}">Maillot</td>
|
<td class="py-4 px-6 font-medium text-gray-900" th:text="${equip.nom}">Maillot</td>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${equip.description != null ? equip.description : '-'}">Maillot de match officiel</td>
|
<td class="py-4 px-6 text-gray-600" th:text="${equip.description != null ? equip.description : '-'}">Maillot de match officiel</td>
|
||||||
<td class="py-4 px-6 text-center">
|
|
||||||
<span class="px-2.5 py-1 text-xs font-semibold rounded-full"
|
|
||||||
th:classappend="${equip.obligatoire ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'}"
|
|
||||||
th:text="${equip.obligatoire ? 'Oui' : 'Non'}">Oui</span>
|
|
||||||
</td>
|
|
||||||
<td class="py-4 px-6 text-center font-medium text-gray-700" th:text="${equip.obligatoire ? 'Inclus' : (equip.prixContribution + ' €')}">30 €</td>
|
|
||||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||||
<a th:href="@{/equipements/{id}/edit(id=${equip.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
<a th:href="@{/equipements/{id}/edit(id=${equip.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
||||||
<form th:action="@{/equipements/{id}/delete(id=${equip.id})}" method="post" onsubmit="return confirm('Supprimer cet équipement ? Cela supprimera également les dotations associées de toutes les licences.');">
|
<form th:action="@{/equipements/{id}/delete(id=${equip.id})}" method="post" onsubmit="return confirm('Supprimer cet équipement ? Cela supprimera également les dotations associées de toutes les licences.');">
|
||||||
@@ -60,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}">N°</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 : '-'}">N°</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>
|
||||||
@@ -30,12 +31,13 @@
|
|||||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||||
<th class="py-3 px-6 font-medium text-left">Nom de la Saison</th>
|
<th class="py-3 px-6 font-medium text-left">Nom de la Saison</th>
|
||||||
<th class="py-3 px-6 font-medium text-center">Statut</th>
|
<th class="py-3 px-6 font-medium text-center">Statut</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-center">Inscriptions Publiques</th>
|
||||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(saisons)}">
|
<tr th:if="${#lists.isEmpty(saisons)}">
|
||||||
<td colspan="3" class="py-8 text-center text-gray-500">Aucune saison configurée.</td>
|
<td colspan="4" class="py-8 text-center text-gray-500">Aucune saison configurée.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="saison : ${saisons}" class="hover:bg-gray-50 transition-colors">
|
<tr th:each="saison : ${saisons}" class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${saison.nom}">Saison 2024-2025</td>
|
<td class="py-4 px-6 font-medium text-gray-900" th:text="${saison.nom}">Saison 2024-2025</td>
|
||||||
@@ -47,6 +49,14 @@
|
|||||||
Clôturée
|
Clôturée
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<form th:if="${saison.estActive}" th:action="@{/saisons/{id}/toggle-inscriptions(id=${saison.id})}" method="post">
|
||||||
|
<button type="submit" th:class="${saison.inscriptionsOuvertes ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'} + ' px-3 py-1 rounded-full text-xs font-medium hover:opacity-80 transition-opacity'" title="Cliquez pour ouvrir/fermer">
|
||||||
|
<span th:text="${saison.inscriptionsOuvertes ? 'Ouvertes' : 'Fermées'}"></span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<span th:unless="${saison.estActive}" class="text-gray-400 text-xs">-</span>
|
||||||
|
</td>
|
||||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||||
<form th:if="${!saison.estActive}" th:action="@{/saisons/{id}/activer(id=${saison.id})}" method="post">
|
<form th:if="${!saison.estActive}" th:action="@{/saisons/{id}/activer(id=${saison.id})}" method="post">
|
||||||
<button type="submit" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Définir Active</button>
|
<button type="submit" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Définir Active</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>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>
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
class="border-l-4 p-2 rounded shadow-sm flex flex-col justify-between overflow-hidden cursor-pointer hover:shadow transition-all relative z-10 m-0.5">
|
class="border-l-4 p-2 rounded shadow-sm flex flex-col justify-between overflow-hidden cursor-pointer hover:shadow transition-all relative z-10 m-0.5">
|
||||||
|
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<span class="font-bold text-xs" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U15</span>
|
<span class="font-bold text-xs" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '') + (c.initialesEducateurs != null and !c.initialesEducateurs.isEmpty() ? ' [' + c.initialesEducateurs + ']' : '')}">U15</span>
|
||||||
<!-- Delete button or info -->
|
<!-- Delete button or info -->
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<a th:href="@{/planning/{id}/edit(id=${c.id})}" class="text-[10px] text-gray-500 hover:underline">Modifier</a>
|
<a th:href="@{/planning/{id}/edit(id=${c.id})}" class="text-[10px] text-gray-500 hover:underline">Modifier</a>
|
||||||
@@ -192,7 +193,13 @@
|
|||||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucun entraînement planifié pour ce jour.</td>
|
<td colspan="6" class="py-8 text-center text-gray-500">Aucun entraînement planifié pour ce jour.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="c : ${creneaux}" class="hover:bg-gray-50 transition-colors">
|
<tr th:each="c : ${creneaux}" class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="py-4 px-6 font-semibold text-gray-900" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U11</td>
|
<td class="py-4 px-6 text-gray-900">
|
||||||
|
<span class="font-semibold" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U11</span>
|
||||||
|
<span th:if="${c.initialesEducateurs != null and !c.initialesEducateurs.isEmpty()}"
|
||||||
|
th:text="${c.initialesEducateurs}"
|
||||||
|
class="ml-2 px-2.5 py-1 bg-blue-100 text-blue-800 rounded text-xs font-semibold"
|
||||||
|
th:title="'Éducateurs : ' + ${c.initialesEducateurs}">JD</span>
|
||||||
|
</td>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${c.terrain.nom}">Terrain Synthétique</td>
|
<td class="py-4 px-6 text-gray-600" th:text="${c.terrain.nom}">Terrain Synthétique</td>
|
||||||
<td class="py-4 px-6 text-gray-600">
|
<td class="py-4 px-6 text-gray-600">
|
||||||
<span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs" th:text="${c.zone.libelle}">Demi A</span>
|
<span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs" th:text="${c.zone.libelle}">Demi A</span>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Pré-inscriptions - 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>
|
||||||
|
<meta name="_csrf" th:content="${_csrf.token}"/>
|
||||||
|
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
|
||||||
|
</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">Dossiers en attente</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-auto p-6">
|
||||||
|
<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-sm 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">Adhérent</th>
|
||||||
|
<th class="py-3 px-6 font-medium">Contact</th>
|
||||||
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
|
<tr th:if="${#lists.isEmpty(preInscriptions)}">
|
||||||
|
<td colspan="4" class="py-8 text-center text-gray-500">Aucune pré-inscription en attente.</td>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="pre : ${preInscriptions}" th:id="'pre-' + ${pre.id}" class="hover:bg-gray-50 transition-colors">
|
||||||
|
<td class="py-4 px-6 text-gray-600" th:text="${#temporals.format(pre.dateCreation, 'dd/MM/yyyy HH:mm')}"></td>
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="font-medium text-gray-900" th:text="${pre.nom + ' ' + pre.prenom}"></div>
|
||||||
|
<div class="text-xs text-gray-500" th:text="'Né(e) le ' + ${#temporals.format(pre.dateNaissance, 'dd/MM/yyyy')}"></div>
|
||||||
|
<div th:if="${pre.representantLegal != null}" class="text-[10px] text-blue-600 font-semibold mt-1" th:text="'Parent : ' + ${pre.representantLegal}"></div>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="text-xs text-gray-500" th:text="${pre.email}"></div>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right space-x-2">
|
||||||
|
<button th:attr="hx-post=@{/admin/pre-inscriptions/{id}/valider(id=${pre.id})}"
|
||||||
|
class="bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700 transition-colors">
|
||||||
|
Créer la fiche
|
||||||
|
</button>
|
||||||
|
<button th:attr="hx-post=@{/admin/pre-inscriptions/{id}/rejeter(id=${pre.id})}, hx-target=|#pre-${pre.id}|"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-confirm="Confirmer le rejet du dossier ?"
|
||||||
|
class="bg-red-50 text-red-600 border border-red-200 px-3 py-1.5 rounded hover:bg-red-100 transition-colors">
|
||||||
|
Rejeter
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.body.addEventListener('htmx:configRequest', function(evt) {
|
||||||
|
evt.detail.headers[document.querySelector('meta[name="_csrf_header"]').content] =
|
||||||
|
document.querySelector('meta[name="_csrf"]').content;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!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>AS Talange - Pré-inscription</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-gray-50 flex items-center justify-center min-h-screen">
|
||||||
|
<div class="max-w-md w-full bg-white rounded-xl shadow-md p-8 border border-gray-100 text-center">
|
||||||
|
<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'">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
Les inscriptions ne sont pas ouvertes pour le moment, veuillez contacter un responsable du club.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p th:unless="${inscriptionsFermees}" class="text-gray-600 mb-8 leading-relaxed">
|
||||||
|
Afin de sécuriser le processus et d'éviter les soumissions automatisées (robots),
|
||||||
|
nous vous demandons de bien vouloir valider le test de sécurité ci-dessous avant de continuer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form th:unless="${inscriptionsFermees}" th:action="@{/inscription-public/demarrer}" method="post" class="flex flex-col items-center">
|
||||||
|
<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
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user