Initial commit - nettoyage et initialisation
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
# Maven
|
||||
**/target/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
.nb-gradle/
|
||||
|
||||
# Python
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.pytest_cache/
|
||||
|
||||
# OS/System
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment/Local secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Build stage
|
||||
FROM maven:3.9.6-eclipse-temurin-21 AS build
|
||||
WORKDIR /app
|
||||
COPY pom.xml .
|
||||
COPY as-talange-common/pom.xml as-talange-common/
|
||||
COPY as-talange-core/pom.xml as-talange-core/
|
||||
COPY as-talange-web/pom.xml as-talange-web/
|
||||
# Copy source code
|
||||
COPY as-talange-common/src as-talange-common/src
|
||||
COPY as-talange-core/src as-talange-core/src
|
||||
COPY as-talange-web/src as-talange-web/src
|
||||
|
||||
# Build the application
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
# Run stage
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/as-talange-web/target/*.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>as-talange-common</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>as-talange-core</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.astalange</groupId>
|
||||
<artifactId>as-talange-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
<version>10.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.77</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import com.astalange.core.validation.ValidRepresentantLegal;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "adherent")
|
||||
@ValidRepresentantLegal
|
||||
public class Adherent {
|
||||
|
||||
@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(name = "date_naissance", nullable = false)
|
||||
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE)
|
||||
private LocalDate dateNaissance;
|
||||
|
||||
@Column(length = 20)
|
||||
private String telephone;
|
||||
|
||||
@Column(length = 100)
|
||||
private String email;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String adresse;
|
||||
|
||||
@Column(name = "representant_legal", length = 150)
|
||||
private String representantLegal;
|
||||
|
||||
private boolean residentTalange = true;
|
||||
|
||||
// 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 LocalDate getDateNaissance() { return dateNaissance; }
|
||||
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
|
||||
|
||||
public String getTelephone() { return telephone; }
|
||||
public void setTelephone(String telephone) { this.telephone = telephone; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getAdresse() { return adresse; }
|
||||
public void setAdresse(String adresse) { this.adresse = adresse; }
|
||||
|
||||
public String getRepresentantLegal() { return representantLegal; }
|
||||
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
|
||||
|
||||
public boolean isResidentTalange() { return residentTalange; }
|
||||
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
|
||||
|
||||
@OneToMany(mappedBy = "adherent", cascade = CascadeType.ALL)
|
||||
private java.util.List<Licence> licences = new java.util.ArrayList<>();
|
||||
|
||||
public java.util.List<Licence> getLicences() { return licences; }
|
||||
public void setLicences(java.util.List<Licence> licences) { this.licences = licences; }
|
||||
|
||||
@Transient
|
||||
public Licence getLicenceActuelle() {
|
||||
if (licences == null || licences.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// Assuming the last added license is the current one (highest ID or latest)
|
||||
return licences.get(licences.size() - 1);
|
||||
}
|
||||
|
||||
@Transient
|
||||
public String getStatutPaiement() {
|
||||
Licence current = getLicenceActuelle();
|
||||
if (current == null) {
|
||||
return "Aucune licence";
|
||||
}
|
||||
java.math.BigDecimal reste = current.getResteAPayer();
|
||||
if (reste.compareTo(java.math.BigDecimal.ZERO) == 0) {
|
||||
return "À jour";
|
||||
} else {
|
||||
return "Reste : " + reste + " €";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "app_user")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AppUser {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled = true;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(
|
||||
name = "user_role",
|
||||
joinColumns = @JoinColumn(name = "user_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||||
)
|
||||
private Set<Role> roles = new HashSet<>();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "categorie")
|
||||
public class Categorie {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 20)
|
||||
private String nom;
|
||||
|
||||
@Column(name = "annee_min")
|
||||
private Integer anneeMin;
|
||||
|
||||
@Column(name = "annee_max")
|
||||
private Integer anneeMax;
|
||||
|
||||
@Column(name = "tarif_base", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal tarifBase;
|
||||
|
||||
@Column(name = "tarif_exterieur", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal tarifExterieur;
|
||||
|
||||
// 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 Integer getAnneeMin() { return anneeMin; }
|
||||
public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; }
|
||||
|
||||
public Integer getAnneeMax() { return anneeMax; }
|
||||
public void setAnneeMax(Integer anneeMax) { this.anneeMax = anneeMax; }
|
||||
|
||||
public BigDecimal getTarifBase() { return tarifBase; }
|
||||
public void setTarifBase(BigDecimal tarifBase) { this.tarifBase = tarifBase; }
|
||||
|
||||
public BigDecimal getTarifExterieur() { return tarifExterieur; }
|
||||
public void setTarifExterieur(BigDecimal tarifExterieur) { this.tarifExterieur = tarifExterieur; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "dotation")
|
||||
public class Dotation {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "licence_id", nullable = false)
|
||||
private Licence licence;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "equipement_id", nullable = false)
|
||||
private Equipement equipement;
|
||||
|
||||
@Column(length = 10)
|
||||
private String taille;
|
||||
|
||||
@Column(length = 50)
|
||||
private String flocage;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean fourni = false;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Licence getLicence() { return licence; }
|
||||
public void setLicence(Licence licence) { this.licence = licence; }
|
||||
|
||||
public Equipement getEquipement() { return equipement; }
|
||||
public void setEquipement(Equipement equipement) { this.equipement = equipement; }
|
||||
|
||||
public String getTaille() { return taille; }
|
||||
public void setTaille(String taille) { this.taille = taille; }
|
||||
|
||||
public String getFlocage() { return flocage; }
|
||||
public void setFlocage(String flocage) { this.flocage = flocage; }
|
||||
|
||||
public Boolean getFourni() { return fourni; }
|
||||
public void setFourni(Boolean fourni) { this.fourni = fourni; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "equipement")
|
||||
public class Equipement {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String nom;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean obligatoire = false;
|
||||
|
||||
// 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 getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public Boolean getObligatoire() { return obligatoire; }
|
||||
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "licence")
|
||||
public class Licence {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "adherent_id", nullable = false)
|
||||
private Adherent adherent;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "categorie_id", nullable = false)
|
||||
private Categorie categorie;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String saison;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
private String etat;
|
||||
|
||||
@Column(name = "numero_licence", length = 50)
|
||||
private String numeroLicence;
|
||||
|
||||
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Paiement> paiements = new ArrayList<>();
|
||||
|
||||
// Logic for Reste A Payer
|
||||
@Transient
|
||||
public BigDecimal getResteAPayer() {
|
||||
if (categorie == null) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange())
|
||||
? categorie.getTarifBase()
|
||||
: categorie.getTarifExterieur();
|
||||
|
||||
if (prixTotal == null) prixTotal = BigDecimal.ZERO;
|
||||
|
||||
if (paiements == null || paiements.isEmpty()) {
|
||||
return prixTotal;
|
||||
}
|
||||
|
||||
BigDecimal totalPaye = paiements.stream()
|
||||
.map(Paiement::getMontant)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal reste = prixTotal.subtract(totalPaye);
|
||||
return reste.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : reste;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Adherent getAdherent() { return adherent; }
|
||||
public void setAdherent(Adherent adherent) { this.adherent = adherent; }
|
||||
|
||||
public Categorie getCategorie() { return categorie; }
|
||||
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
||||
|
||||
public String getSaison() { return saison; }
|
||||
public void setSaison(String saison) { this.saison = saison; }
|
||||
|
||||
public String getEtat() { return etat; }
|
||||
public void setEtat(String etat) { this.etat = etat; }
|
||||
|
||||
public List<Paiement> getPaiements() { return paiements; }
|
||||
public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; }
|
||||
|
||||
public String getNumeroLicence() { return numeroLicence; }
|
||||
public void setNumeroLicence(String numeroLicence) { this.numeroLicence = numeroLicence; }
|
||||
|
||||
public void addPaiement(Paiement paiement) {
|
||||
paiements.add(paiement);
|
||||
paiement.setLicence(this);
|
||||
}
|
||||
|
||||
public void removePaiement(Paiement paiement) {
|
||||
paiements.remove(paiement);
|
||||
paiement.setLicence(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "mode_paiement")
|
||||
public class ModePaiement {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String nom;
|
||||
|
||||
// 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; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "paiement")
|
||||
public class Paiement {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "licence_id", nullable = false)
|
||||
private Licence licence;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal montant;
|
||||
|
||||
@Column(name = "date_paiement", nullable = false)
|
||||
private LocalDate datePaiement;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "mode_paiement_id", nullable = false)
|
||||
private ModePaiement modePaiement;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Licence getLicence() { return licence; }
|
||||
public void setLicence(Licence licence) { this.licence = licence; }
|
||||
|
||||
public BigDecimal getMontant() { return montant; }
|
||||
public void setMontant(BigDecimal montant) { this.montant = montant; }
|
||||
|
||||
public LocalDate getDatePaiement() { return datePaiement; }
|
||||
public void setDatePaiement(LocalDate datePaiement) { this.datePaiement = datePaiement; }
|
||||
|
||||
public ModePaiement getModePaiement() { return modePaiement; }
|
||||
public void setModePaiement(ModePaiement modePaiement) { this.modePaiement = modePaiement; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "role")
|
||||
@Getter
|
||||
@Setter
|
||||
public class Role {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface AdherentRepository extends JpaRepository<Adherent, Long> {
|
||||
List<Adherent> findTop5ByOrderByIdDesc();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.AppUser;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
|
||||
Optional<AppUser> findByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface CategorieRepository extends JpaRepository<Categorie, Long> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Dotation;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface DotationRepository extends JpaRepository<Dotation, Long> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Equipement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface EquipementRepository extends JpaRepository<Equipement, Long> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Licence;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface LicenceRepository extends JpaRepository<Licence, Long> {
|
||||
List<Licence> findByAdherentId(Long adherentId);
|
||||
|
||||
long countByEtat(String etat);
|
||||
|
||||
@Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c")
|
||||
BigDecimal sumTarifBase();
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.ModePaiement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ModePaiementRepository extends JpaRepository<ModePaiement, Long> {
|
||||
Optional<ModePaiement> findByNom(String nom);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Paiement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Repository
|
||||
public interface PaiementRepository extends JpaRepository<Paiement, Long> {
|
||||
|
||||
@Query("SELECT SUM(p.montant) FROM Paiement p")
|
||||
BigDecimal sumMontant();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.astalange.core.security;
|
||||
|
||||
import com.astalange.core.entity.AppUser;
|
||||
import com.astalange.core.repository.AppUserRepository;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
|
||||
public UserDetailsServiceImpl(AppUserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
AppUser appUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
return new User(
|
||||
appUser.getUsername(),
|
||||
appUser.getPassword(),
|
||||
appUser.isEnabled(),
|
||||
true, true, true,
|
||||
appUser.getRoles().stream()
|
||||
.map(role -> new SimpleGrantedAuthority(role.getName()))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.astalange.core.validation;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
|
||||
public class RepresentantLegalValidator implements ConstraintValidator<ValidRepresentantLegal, Adherent> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(Adherent adherent, ConstraintValidatorContext context) {
|
||||
if (adherent == null || adherent.getDateNaissance() == null) {
|
||||
return true; // Let @NotNull handle the null cases
|
||||
}
|
||||
|
||||
int age = Period.between(adherent.getDateNaissance(), LocalDate.now()).getYears();
|
||||
|
||||
if (age < 18) {
|
||||
return adherent.getRepresentantLegal() != null && !adherent.getRepresentantLegal().trim().isEmpty();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.astalange.core.validation;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = RepresentantLegalValidator.class)
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidRepresentantLegal {
|
||||
String message() default "Le représentant légal est obligatoire pour les adhérents mineurs.";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
CREATE TABLE role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE app_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE TABLE user_role (
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE adherent (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nom VARCHAR(100) NOT NULL,
|
||||
prenom VARCHAR(100) NOT NULL,
|
||||
date_naissance DATE NOT NULL,
|
||||
telephone VARCHAR(20),
|
||||
email VARCHAR(100),
|
||||
adresse TEXT,
|
||||
representant_legal VARCHAR(150)
|
||||
);
|
||||
|
||||
CREATE TABLE categorie (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nom VARCHAR(20) UNIQUE NOT NULL,
|
||||
annee_min INT,
|
||||
annee_max INT,
|
||||
tarif_base DECIMAL(10,2) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE licence (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
adherent_id BIGINT NOT NULL,
|
||||
categorie_id BIGINT NOT NULL,
|
||||
saison VARCHAR(20) NOT NULL,
|
||||
etat VARCHAR(50) NOT NULL,
|
||||
CONSTRAINT fk_licence_adherent FOREIGN KEY (adherent_id) REFERENCES adherent(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_licence_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id)
|
||||
);
|
||||
|
||||
CREATE TABLE paiement (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
licence_id BIGINT NOT NULL,
|
||||
montant DECIMAL(10,2) NOT NULL,
|
||||
date_paiement DATE NOT NULL,
|
||||
mode_paiement VARCHAR(50) NOT NULL,
|
||||
CONSTRAINT fk_paiement_licence FOREIGN KEY (licence_id) REFERENCES licence(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE equipement (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nom VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
obligatoire BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE dotation (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
licence_id BIGINT NOT NULL,
|
||||
equipement_id BIGINT NOT NULL,
|
||||
taille VARCHAR(10),
|
||||
flocage VARCHAR(50),
|
||||
fourni BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
CONSTRAINT fk_dotation_licence FOREIGN KEY (licence_id) REFERENCES licence(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_dotation_equipement FOREIGN KEY (equipement_id) REFERENCES equipement(id)
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE mode_paiement (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nom VARCHAR(50) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO mode_paiement (nom) VALUES ('Espèces'), ('Chèque'), ('Pass Sport'), ('Virement');
|
||||
|
||||
ALTER TABLE paiement ADD COLUMN mode_paiement_id BIGINT;
|
||||
|
||||
ALTER TABLE paiement ADD CONSTRAINT fk_paiement_mode FOREIGN KEY (mode_paiement_id) REFERENCES mode_paiement(id);
|
||||
|
||||
ALTER TABLE paiement DROP COLUMN mode_paiement;
|
||||
@@ -0,0 +1,23 @@
|
||||
INSERT INTO role (name) VALUES ('ROLE_ADMIN'), ('ROLE_USER');
|
||||
|
||||
-- Password is "Talange2026!" hashed with Argon2id
|
||||
INSERT INTO app_user (username, password, enabled) VALUES ('Ucef', '$argon2id$v=19$m=65536,t=3,p=4$q29E/VM1esZVxo9HufgILQ$Nzb+rwwsZUMy04BbewYum2A8L8ujs9VZj9VJ/609Am0', true);
|
||||
|
||||
INSERT INTO user_role (user_id, role_id) VALUES ((SELECT id FROM app_user WHERE username = 'Ucef'), (SELECT id FROM role WHERE name = 'ROLE_ADMIN'));
|
||||
|
||||
INSERT INTO categorie (nom, annee_min, annee_max, tarif_base) VALUES
|
||||
('U6', 2020, 2020, 100.00),
|
||||
('U7', 2019, 2019, 100.00),
|
||||
('U8', 2018, 2018, 120.00),
|
||||
('U9', 2017, 2017, 120.00),
|
||||
('U10', 2016, 2016, 130.00),
|
||||
('U11', 2015, 2015, 130.00),
|
||||
('U12', 2014, 2014, 140.00),
|
||||
('U13', 2013, 2013, 140.00),
|
||||
('Senior', 1900, 2005, 180.00);
|
||||
|
||||
INSERT INTO equipement (nom, obligatoire) VALUES
|
||||
('Maillot', true),
|
||||
('Short', true),
|
||||
('Chaussettes', true),
|
||||
('Survêtement', false);
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Ajouter la colonne pour savoir si l'adhérent est résident de Talange (par défaut oui pour les existants)
|
||||
ALTER TABLE adherent ADD COLUMN resident_talange BOOLEAN DEFAULT TRUE;
|
||||
|
||||
-- Ajouter le tarif extérieur aux catégories
|
||||
ALTER TABLE categorie ADD COLUMN tarif_exterieur DECIMAL(10,2);
|
||||
|
||||
-- Par défaut, mettre le tarif extérieur égal au tarif de base pour ne pas casser les licences existantes
|
||||
UPDATE categorie SET tarif_exterieur = tarif_base;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE licence ADD COLUMN numero_licence VARCHAR(50);
|
||||
@@ -0,0 +1,11 @@
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
|
||||
public class HashGen {
|
||||
public static void main(String[] args) {
|
||||
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
String result = encoder.encode("Talange2026!");
|
||||
System.out.println("HASH_RESULT_START");
|
||||
System.out.println(result);
|
||||
System.out.println("HASH_RESULT_END");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>as-talange-web</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.astalange</groupId>
|
||||
<artifactId>as-talange-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>com.astalange.web.AsTalangeApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.astalange.web;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.astalange")
|
||||
@EntityScan(basePackages = "com.astalange.core.entity")
|
||||
@EnableJpaRepositories(basePackages = "com.astalange.core.repository")
|
||||
public class AsTalangeApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AsTalangeApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.astalange.web.config;
|
||||
|
||||
import com.astalange.core.security.UserDetailsServiceImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.defaultSuccessUrl("/", true)
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessUrl("/login?logout")
|
||||
.permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/adherents")
|
||||
public class AdherentController {
|
||||
|
||||
private final AdherentRepository adherentRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final com.astalange.core.repository.LicenceRepository licenceRepository;
|
||||
private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository;
|
||||
|
||||
public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.modePaiementRepository = modePaiementRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listAdherents(Model model) {
|
||||
List<Adherent> adherents = adherentRepository.findAll();
|
||||
model.addAttribute("adherents", adherents);
|
||||
return "adherents/list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("adherent", new Adherent());
|
||||
return "adherents/form";
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.PostMapping
|
||||
public String saveAdherent(
|
||||
@jakarta.validation.Valid @org.springframework.web.bind.annotation.ModelAttribute("adherent") Adherent adherent,
|
||||
org.springframework.validation.BindingResult result,
|
||||
Model model) {
|
||||
|
||||
if (result.hasErrors()) {
|
||||
return "adherents/form";
|
||||
}
|
||||
|
||||
adherentRepository.save(adherent);
|
||||
return "redirect:/adherents";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) {
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
|
||||
model.addAttribute("adherent", adherent);
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
model.addAttribute("licences", licenceRepository.findByAdherentId(id));
|
||||
model.addAttribute("modesPaiement", modePaiementRepository.findAll());
|
||||
return "adherents/form";
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.PostMapping("/{id}/delete")
|
||||
public String deleteAdherent(@org.springframework.web.bind.annotation.PathVariable Long id) {
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
|
||||
adherentRepository.delete(adherent);
|
||||
return "redirect:/adherents";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/categories")
|
||||
public class CategorieController {
|
||||
|
||||
private final CategorieRepository categorieRepository;
|
||||
|
||||
public CategorieController(CategorieRepository categorieRepository) {
|
||||
this.categorieRepository = categorieRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listCategories(Model model) {
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
return "parametrage/categories_list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("categorie", new Categorie());
|
||||
return "parametrage/categories_form";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
Categorie categorie = categorieRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
|
||||
model.addAttribute("categorie", categorie);
|
||||
return "parametrage/categories_form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie) {
|
||||
categorieRepository.save(categorie);
|
||||
return "redirect:/categories";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteCategorie(@PathVariable Long id) {
|
||||
Categorie categorie = categorieRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
|
||||
categorieRepository.delete(categorie);
|
||||
return "redirect:/categories";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import com.astalange.core.repository.PaiementRepository;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
private final AdherentRepository adherentRepository;
|
||||
private final LicenceRepository licenceRepository;
|
||||
private final PaiementRepository paiementRepository;
|
||||
|
||||
public HomeController(AdherentRepository adherentRepository, LicenceRepository licenceRepository, PaiementRepository paiementRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.paiementRepository = paiementRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model, Authentication authentication) {
|
||||
if (authentication != null) {
|
||||
model.addAttribute("username", authentication.getName());
|
||||
}
|
||||
|
||||
long totalAdherents = adherentRepository.count();
|
||||
long licencesBrouillon = licenceRepository.countByEtat("Brouillon");
|
||||
long licencesValidees = licenceRepository.countByEtat("Validée");
|
||||
long licencesPayees = licenceRepository.countByEtat("Payée");
|
||||
|
||||
BigDecimal totalEncaisse = paiementRepository.sumMontant();
|
||||
if (totalEncaisse == null) totalEncaisse = BigDecimal.ZERO;
|
||||
|
||||
List<com.astalange.core.entity.Licence> allLicences = licenceRepository.findAll();
|
||||
BigDecimal totalAttendu = allLicences.stream()
|
||||
.map(lic -> {
|
||||
if (lic.getCategorie() == null) return BigDecimal.ZERO;
|
||||
return (lic.getAdherent() != null && lic.getAdherent().isResidentTalange())
|
||||
? lic.getCategorie().getTarifBase()
|
||||
: lic.getCategorie().getTarifExterieur();
|
||||
})
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal resteARecouvrer = totalAttendu.subtract(totalEncaisse);
|
||||
if (resteARecouvrer.compareTo(BigDecimal.ZERO) < 0) resteARecouvrer = BigDecimal.ZERO;
|
||||
|
||||
List<Adherent> derniersInscrits = adherentRepository.findTop5ByOrderByIdDesc();
|
||||
|
||||
model.addAttribute("totalAdherents", totalAdherents);
|
||||
model.addAttribute("licencesBrouillon", licencesBrouillon);
|
||||
model.addAttribute("licencesActives", licencesValidees + licencesPayees);
|
||||
model.addAttribute("totalEncaisse", totalEncaisse);
|
||||
model.addAttribute("resteARecouvrer", resteARecouvrer);
|
||||
model.addAttribute("derniersInscrits", derniersInscrits);
|
||||
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.entity.Licence;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class LicenceController {
|
||||
|
||||
private final AdherentRepository adherentRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final LicenceRepository licenceRepository;
|
||||
|
||||
public LicenceController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, LicenceRepository licenceRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/adherents/{id}/licences")
|
||||
public String addLicence(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long categorieId,
|
||||
@RequestParam String saison,
|
||||
@RequestParam String etat) {
|
||||
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Adherent ID"));
|
||||
Categorie categorie = categorieRepository.findById(categorieId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Categorie ID"));
|
||||
|
||||
Licence licence = new Licence();
|
||||
licence.setAdherent(adherent);
|
||||
licence.setCategorie(categorie);
|
||||
licence.setSaison(saison);
|
||||
licence.setEtat(etat);
|
||||
|
||||
licenceRepository.save(licence);
|
||||
|
||||
return "redirect:/adherents/" + id + "/edit";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.ModePaiement;
|
||||
import com.astalange.core.repository.ModePaiementRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/modespaiement")
|
||||
public class ModePaiementController {
|
||||
|
||||
private final ModePaiementRepository modePaiementRepository;
|
||||
|
||||
public ModePaiementController(ModePaiementRepository modePaiementRepository) {
|
||||
this.modePaiementRepository = modePaiementRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listModes(Model model) {
|
||||
model.addAttribute("modes", modePaiementRepository.findAll());
|
||||
return "parametrage/modespaiement_list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("modePaiement", new ModePaiement());
|
||||
return "parametrage/modespaiement_form";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
ModePaiement mode = modePaiementRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid ID: " + id));
|
||||
model.addAttribute("modePaiement", mode);
|
||||
return "parametrage/modespaiement_form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String saveMode(@ModelAttribute("modePaiement") ModePaiement modePaiement) {
|
||||
modePaiementRepository.save(modePaiement);
|
||||
return "redirect:/modespaiement";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteMode(@PathVariable Long id) {
|
||||
ModePaiement mode = modePaiementRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid ID: " + id));
|
||||
modePaiementRepository.delete(mode);
|
||||
return "redirect:/modespaiement";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Licence;
|
||||
import com.astalange.core.entity.ModePaiement;
|
||||
import com.astalange.core.entity.Paiement;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import com.astalange.core.repository.ModePaiementRepository;
|
||||
import com.astalange.core.repository.PaiementRepository;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Controller
|
||||
public class PaiementController {
|
||||
|
||||
private final LicenceRepository licenceRepository;
|
||||
private final ModePaiementRepository modePaiementRepository;
|
||||
private final PaiementRepository paiementRepository;
|
||||
|
||||
public PaiementController(LicenceRepository licenceRepository, ModePaiementRepository modePaiementRepository, PaiementRepository paiementRepository) {
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.modePaiementRepository = modePaiementRepository;
|
||||
this.paiementRepository = paiementRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/licences/{id}/paiements")
|
||||
public String addPaiement(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long adherentId,
|
||||
@RequestParam BigDecimal montant,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
|
||||
@RequestParam Long modePaiementId) {
|
||||
|
||||
Licence licence = licenceRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
|
||||
ModePaiement mode = modePaiementRepository.findById(modePaiementId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid ModePaiement ID"));
|
||||
|
||||
// Validation basique pour ne pas payer plus que le reste à payer
|
||||
if (montant.compareTo(licence.getResteAPayer()) > 0) {
|
||||
montant = licence.getResteAPayer();
|
||||
}
|
||||
|
||||
if (montant.compareTo(BigDecimal.ZERO) > 0) {
|
||||
Paiement paiement = new Paiement();
|
||||
paiement.setLicence(licence);
|
||||
paiement.setModePaiement(mode);
|
||||
paiement.setMontant(montant);
|
||||
paiement.setDatePaiement(datePaiement);
|
||||
paiementRepository.save(paiement);
|
||||
}
|
||||
|
||||
return "redirect:/adherents/" + adherentId + "/edit";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
spring:
|
||||
application:
|
||||
name: as-talange
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5433/astalange
|
||||
username: myuser
|
||||
password: mypassword
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
@@ -0,0 +1,328 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nouvel Adhérent - 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; }
|
||||
.hidden-block { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Adhérents</a>
|
||||
<a href="#" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
<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>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 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" th:text="${adherent.id == null ? 'Ajouter un adhérent' : 'Modifier l''adhérent'}">Ajouter un adhérent</h2>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<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">
|
||||
|
||||
<form th:action="@{/adherents}" th:object="${adherent}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div th:if="${#fields.hasErrors('global')}" class="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg text-sm mb-6">
|
||||
<p th:each="err : ${#fields.errors('global')}" th:text="${err}"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Nom -->
|
||||
<div>
|
||||
<label for="nom" class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="nom" 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 focus:border-blue-500 outline-none transition-colors"
|
||||
th:classappend="${#fields.hasErrors('nom')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Prénom -->
|
||||
<div>
|
||||
<label for="prenom" class="block text-sm font-medium text-gray-700 mb-1">Prénom <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="prenom" 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 focus:border-blue-500 outline-none transition-colors"
|
||||
th:classappend="${#fields.hasErrors('prenom')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('prenom')}" th:errors="*{prenom}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Date de Naissance -->
|
||||
<div>
|
||||
<label for="dateNaissance" class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
|
||||
<input type="date" id="dateNaissance" th:field="*{dateNaissance}" 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('dateNaissance')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('dateNaissance')}" th:errors="*{dateNaissance}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Téléphone -->
|
||||
<div>
|
||||
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
|
||||
<input type="tel" id="telephone" th:field="*{telephone}"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||
th:classappend="${#fields.hasErrors('telephone')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('telephone')}" th:errors="*{telephone}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Représentant Légal (Dynamique) -->
|
||||
<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">
|
||||
Représentant légal <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p class="text-xs text-blue-600 mb-2">Obligatoire car l'adhérent a moins de 18 ans.</p>
|
||||
<input type="text" id="representantLegal" th:field="*{representantLegal}"
|
||||
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('representantLegal')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('representantLegal')}" th:errors="*{representantLegal}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<input type="email" id="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 focus:border-blue-500 outline-none transition-colors"
|
||||
th:classappend="${#fields.hasErrors('email')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Résident Talange -->
|
||||
<div class="flex items-center space-x-3 mt-7">
|
||||
<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="residentTalange" class="text-sm font-medium text-gray-700">
|
||||
Résident de la commune de Talange
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adresse -->
|
||||
<div>
|
||||
<label for="adresse" class="block text-sm font-medium text-gray-700 mb-1">Adresse postale</label>
|
||||
<textarea id="adresse" th:field="*{adresse}" rows="3"
|
||||
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('adresse')} ? 'border-red-500' : ''"></textarea>
|
||||
<p th:if="${#fields.hasErrors('adresse')}" th:errors="*{adresse}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Licences de l'adhérent</h3>
|
||||
<button onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
|
||||
+ Ajouter une Licence
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<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">N° Licence</th>
|
||||
<th class="py-3 px-4 font-medium">État</th>
|
||||
<th class="py-3 px-4 font-medium">Reste à Payer</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="6" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
|
||||
</tr>
|
||||
<th:block th:each="lic : ${licences}">
|
||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td>
|
||||
<td class="py-4 px-4 text-gray-600" th:text="${lic.categorie.nom}">U15</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">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
th:classappend="${lic.etat == 'Validée' ? 'bg-blue-100 text-blue-800' : (lic.etat == 'Payée' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800')}"
|
||||
th:text="${lic.etat}">Brouillon</span>
|
||||
</td>
|
||||
<td class="py-4 px-4 text-gray-600 font-medium" th:text="${lic.getResteAPayer() + ' €'}">100.00 €</td>
|
||||
<td class="py-4 px-4 text-right">
|
||||
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'"
|
||||
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">Payer</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
|
||||
<td colspan="5" class="py-2 px-4">
|
||||
<div class="text-xs text-gray-500 mb-1 font-medium uppercase tracking-wider">Historique des paiements</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<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">
|
||||
<span th:text="${paiement.montant + ' €'}">50 €</span>
|
||||
<span class="mx-1 text-gray-300">|</span>
|
||||
<span th:text="${paiement.modePaiement.nom}">Chèque</span>
|
||||
<span class="mx-1 text-gray-300">|</span>
|
||||
<span th:text="${#temporals.format(paiement.datePaiement, 'dd/MM/yyyy')}">01/01/2026</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</th:block>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Licence Modal -->
|
||||
<div id="licenceModal" 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">Nouvelle Licence</h3>
|
||||
<form th:if="${adherent.id != null}" th:action="@{/adherents/{id}/licences(id=${adherent.id})}" method="post" class="space-y-4 text-left">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
|
||||
<select name="categorieId" 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 une catégorie...</option>
|
||||
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom} + ' (' + ${cat.tarifBase} + ' €)'"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
|
||||
<input 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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Saison</label>
|
||||
<input type="text" name="saison" required value="2024-2025" 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">État</label>
|
||||
<select name="etat" 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="Brouillon">Brouillon</option>
|
||||
<option value="Validée">Validée</option>
|
||||
<option value="Payée">Payée</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="items-center px-4 py-3 flex justify-end space-x-2">
|
||||
<button type="button" onclick="closeLicenceModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
|
||||
<button type="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>
|
||||
|
||||
<!-- Paiement Modal -->
|
||||
<div id="paiementModal" 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">Enregistrer un Paiement</h3>
|
||||
<form id="paiementForm" 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" id="paiementMontant" 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="paiementDate" 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 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="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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openLicenceModal() {
|
||||
document.getElementById('licenceModal').classList.remove('hidden');
|
||||
}
|
||||
function closeLicenceModal() {
|
||||
document.getElementById('licenceModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function openPaiementModal(licenceId, maxAmount) {
|
||||
document.getElementById('paiementModal').classList.remove('hidden');
|
||||
document.getElementById('paiementForm').action = '/licences/' + licenceId + '/paiements';
|
||||
document.getElementById('paiementMontant').value = maxAmount;
|
||||
document.getElementById('paiementMontant').max = maxAmount;
|
||||
document.getElementById('paiementDate').valueAsDate = new Date();
|
||||
}
|
||||
function closePaiementModal() {
|
||||
document.getElementById('paiementModal').classList.add('hidden');
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const dateInput = document.getElementById('dateNaissance');
|
||||
const repBlock = document.getElementById('representantBlock');
|
||||
|
||||
function calculateAge(birthday) {
|
||||
const ageDifMs = Date.now() - birthday.getTime();
|
||||
const ageDate = new Date(ageDifMs);
|
||||
return Math.abs(ageDate.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
function updateRepresentantBlock() {
|
||||
if (dateInput.value) {
|
||||
const dob = new Date(dateInput.value);
|
||||
const age = calculateAge(dob);
|
||||
if (age < 18) {
|
||||
repBlock.classList.remove('hidden-block');
|
||||
} else {
|
||||
repBlock.classList.add('hidden-block');
|
||||
}
|
||||
} else {
|
||||
repBlock.classList.add('hidden-block');
|
||||
}
|
||||
}
|
||||
|
||||
dateInput.addEventListener('change', updateRepresentantBlock);
|
||||
|
||||
// Initial check in case it's a validation error reload
|
||||
updateRepresentantBlock();
|
||||
// But if there's already an error on representantLegal, ensure it is shown
|
||||
const hasRepError = document.querySelector('p[th\\:errors="*{representantLegal}"]');
|
||||
if (hasRepError || document.getElementById('representantLegal').classList.contains('border-red-500')) {
|
||||
repBlock.classList.remove('hidden-block');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Adhérents - 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 -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Adhérents</a>
|
||||
<a href="#" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
<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>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 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">Gestion des Adhérents</h2>
|
||||
<button class="md:hidden text-gray-500 hover:text-gray-700">
|
||||
<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="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<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 Adhérents</h3>
|
||||
<a th:href="@{/adherents/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors inline-block">
|
||||
+ Nouvel Adhérent
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div 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">
|
||||
<input type="text" placeholder="Rechercher un adhérent..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<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-center">N° Licence</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Email</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Téléphone</th>
|
||||
<th class="py-3 px-6 font-medium text-center">Paiement</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(adherents)}">
|
||||
<td colspan="7" class="py-8 text-center text-gray-500">Aucun adhérent enregistré.</td>
|
||||
</tr>
|
||||
<tr th:each="adherent : ${adherents}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.nom}">Dupont</td>
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.prenom}">Jean</td>
|
||||
<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-4 px-6 text-gray-600" th:text="${adherent.telephone}">0600000000</td>
|
||||
<td class="py-4 px-6 text-center">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tableau de bord - 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 -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Adhérents</a>
|
||||
<a href="#" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="text-sm font-medium text-gray-900 mb-1" th:text="${username}">Admin</div>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 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">Tableau de bord</h2>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-50/50">
|
||||
|
||||
<!-- KPIs -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Adhérents -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
|
||||
<div class="p-3 bg-blue-50 text-blue-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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Total Adhérents</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${totalAdherents}">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Licences Actives -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
|
||||
<div class="p-3 bg-green-50 text-green-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Licences Actives</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${licencesActives}">0</p>
|
||||
<p class="text-xs text-gray-400 mt-1" th:text="${licencesBrouillon} + ' en brouillon'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Encaissé -->
|
||||
<div 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">
|
||||
<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>
|
||||
<p class="text-sm font-medium text-gray-500">Total Encaissé</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${totalEncaisse} + ' €'">0 €</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reste à Recouvrer -->
|
||||
<div 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">
|
||||
<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>
|
||||
<p class="text-sm font-medium text-gray-500">Reste à Recouvrer</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${resteARecouvrer} + ' €'">0 €</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Derniers Inscrits -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<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>
|
||||
</div>
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 text-sm">
|
||||
<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>
|
||||
</tr>
|
||||
<tr th:each="adh : ${derniersInscrits}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6">
|
||||
<div class="font-medium text-gray-900" th:text="${adh.nom + ' ' + adh.prenom}">Dupont Jean</div>
|
||||
<div class="text-xs text-gray-500" th:text="${adh.email}">jean@example.com</div>
|
||||
</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">
|
||||
<a th:href="@{/adherents/{id}/edit(id=${adh.id})}" class="text-blue-600 hover:text-blue-800 font-medium">Voir profil</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Connexion - 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 flex items-center justify-center min-h-screen">
|
||||
|
||||
<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">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">AS Talange</h1>
|
||||
<p class="text-gray-500">Outil de gestion administrative</p>
|
||||
</div>
|
||||
|
||||
<div th:if="${param.error}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm">
|
||||
Identifiant ou mot de passe incorrect.
|
||||
</div>
|
||||
|
||||
<div th:if="${param.logout}" class="bg-green-50 text-green-600 p-4 rounded-lg mb-6 text-sm">
|
||||
Vous avez été déconnecté avec succès.
|
||||
</div>
|
||||
|
||||
<form th:action="@{/login}" method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Nom d'utilisateur</label>
|
||||
<input type="text" id="username" name="username" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required autofocus>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200">
|
||||
Se connecter
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Catégorie - 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 -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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="${categorie.id == null ? 'Nouvelle Catégorie' : 'Modifier Catégorie'}">Catégorie</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="@{/categories}" th:object="${categorie}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label>
|
||||
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Âge Min</label>
|
||||
<input type="number" th:field="*{anneeMin}" 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">Âge Max</label>
|
||||
<input type="number" th:field="*{anneeMax}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="tarifBase" class="block text-sm font-medium text-gray-700 mb-1">Tarif de base (Talangeois) (€) <span class="text-red-500">*</span></label>
|
||||
<input type="number" step="0.01" id="tarifBase" th:field="*{tarifBase}" 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">
|
||||
</div>
|
||||
<div>
|
||||
<label for="tarifExterieur" class="block text-sm font-medium text-gray-700 mb-1">Tarif Extérieur (hors commune de Talange) (€) <span class="text-red-500">*</span></label>
|
||||
<input type="number" step="0.01" id="tarifExterieur" th:field="*{tarifExterieur}" 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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paramétrage - Catégories</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 -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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">Catégories Sportives</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 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">
|
||||
+ Nouvelle Catégorie
|
||||
</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 de la catégorie</th>
|
||||
<th class="py-3 px-6 font-medium text-center">Année Min</th>
|
||||
<th class="py-3 px-6 font-medium text-center">Année Max</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Tarif (Talangeois)</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Tarif (Hors Commune)</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(categories)}">
|
||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td>
|
||||
</tr>
|
||||
<tr th:each="cat : ${categories}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${cat.nom}">U15</td>
|
||||
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMin}">2010</td>
|
||||
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMax}">2011</td>
|
||||
<td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td>
|
||||
<td class="py-4 px-6 text-right font-medium text-purple-600" th:text="${cat.tarifExterieur != null ? cat.tarifExterieur + ' €' : '-'}">130.00 €</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||
<a th:href="@{/categories/{id}/edit(id=${cat.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
||||
<form th:action="@{/categories/{id}/delete(id=${cat.id})}" method="post" onsubmit="return confirm('Supprimer cette catégorie ?');">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mode de Paiement - 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 -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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="${modePaiement.id == null ? 'Nouveau Mode' : 'Modifier Mode'}">Mode de Paiement</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="@{/modespaiement}" th:object="${modePaiement}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: Chèque)</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 class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<a th:href="@{/modespaiement}" 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,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paramétrage - Modes de Paiement</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 -->
|
||||
<aside 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">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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">Modes de Paiement</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 Modes de Paiement</h3>
|
||||
<a th:href="@{/modespaiement/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||
+ Nouveau Mode
|
||||
</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">ID</th>
|
||||
<th class="py-3 px-6 font-medium">Nom</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(modes)}">
|
||||
<td colspan="3" class="py-4 px-6 text-center text-gray-500">Aucun mode de paiement trouvé.</td>
|
||||
</tr>
|
||||
<tr th:each="mode : ${modes}" class="hover:bg-gray-50">
|
||||
<td class="py-4 px-6 text-gray-500" th:text="${mode.id}">1</td>
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${mode.nom}">Chèque</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||
<a th:href="@{/modespaiement/{id}/edit(id=${mode.id})}" class="text-blue-600 hover:text-blue-800 font-medium">Éditer</a>
|
||||
<form th:action="@{/modespaiement/{id}/delete(id=${mode.id})}" method="post" onsubmit="return confirm('Supprimer ce mode de paiement ?');">
|
||||
<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>
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
container_name: astalange_db
|
||||
environment:
|
||||
POSTGRES_USER: myuser
|
||||
POSTGRES_PASSWORD: mypassword
|
||||
POSTGRES_DB: astalange
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
container_name: astalange_pgadmin
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@astalange.com
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
ports:
|
||||
- "5050:80"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>com.astalange</groupId>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>as-talange-parent</name>
|
||||
<description>AS Talange Management Application</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>as-talange-common</module>
|
||||
<module>as-talange-core</module>
|
||||
<module>as-talange-web</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
argon2-cffi==25.1.0
|
||||
argon2-cffi-bindings==25.1.0
|
||||
cffi==2.0.0
|
||||
pycparser==3.0
|
||||
Reference in New Issue
Block a user