commit ee85d38c43dfacfa49979f9d6f30c08d94f9f08b Author: Youssef Date: Tue May 19 22:01:18 2026 +0200 Initial commit - nettoyage et initialisation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a99a60 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..98eed7a --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/as-talange-common/pom.xml b/as-talange-common/pom.xml new file mode 100644 index 0000000..5aad3fb --- /dev/null +++ b/as-talange-common/pom.xml @@ -0,0 +1,21 @@ + + + + as-talange-parent + com.astalange + 1.0-SNAPSHOT + + 4.0.0 + + as-talange-common + + + + org.projectlombok + lombok + true + + + diff --git a/as-talange-core/pom.xml b/as-talange-core/pom.xml new file mode 100644 index 0000000..5f80c92 --- /dev/null +++ b/as-talange-core/pom.xml @@ -0,0 +1,65 @@ + + + + as-talange-parent + com.astalange + 1.0-SNAPSHOT + + 4.0.0 + + as-talange-core + + + + com.astalange + as-talange-common + ${project.version} + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.projectlombok + lombok + true + + + + org.postgresql + postgresql + runtime + + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + 10.10.0 + + + + org.springframework.boot + spring-boot-starter-security + + + + org.bouncycastle + bcprov-jdk18on + 1.77 + + + + diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Adherent.java b/as-talange-core/src/main/java/com/astalange/core/entity/Adherent.java new file mode 100644 index 0000000..99103a6 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Adherent.java @@ -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 licences = new java.util.ArrayList<>(); + + public java.util.List getLicences() { return licences; } + public void setLicences(java.util.List 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 + " €"; + } + } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java b/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java new file mode 100644 index 0000000..ea06dba --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java @@ -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 roles = new HashSet<>(); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Categorie.java b/as-talange-core/src/main/java/com/astalange/core/entity/Categorie.java new file mode 100644 index 0000000..54ea636 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Categorie.java @@ -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; } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Dotation.java b/as-talange-core/src/main/java/com/astalange/core/entity/Dotation.java new file mode 100644 index 0000000..2312861 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Dotation.java @@ -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; } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Equipement.java b/as-talange-core/src/main/java/com/astalange/core/entity/Equipement.java new file mode 100644 index 0000000..e9d03be --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Equipement.java @@ -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; } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java b/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java new file mode 100644 index 0000000..51108a9 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Licence.java @@ -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 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 getPaiements() { return paiements; } + public void setPaiements(List 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); + } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/ModePaiement.java b/as-talange-core/src/main/java/com/astalange/core/entity/ModePaiement.java new file mode 100644 index 0000000..4e5a05b --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/ModePaiement.java @@ -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; } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Paiement.java b/as-talange-core/src/main/java/com/astalange/core/entity/Paiement.java new file mode 100644 index 0000000..7ad019e --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Paiement.java @@ -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; } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/Role.java b/as-talange-core/src/main/java/com/astalange/core/entity/Role.java new file mode 100644 index 0000000..d1d6964 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/entity/Role.java @@ -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; +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/AdherentRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/AdherentRepository.java new file mode 100644 index 0000000..9469049 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/AdherentRepository.java @@ -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 { + List findTop5ByOrderByIdDesc(); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/AppUserRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/AppUserRepository.java new file mode 100644 index 0000000..fad45ee --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/AppUserRepository.java @@ -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 { + Optional findByUsername(String username); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/CategorieRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/CategorieRepository.java new file mode 100644 index 0000000..697cb18 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/CategorieRepository.java @@ -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 { +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java new file mode 100644 index 0000000..0f6d83b --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/DotationRepository.java @@ -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 { +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/EquipementRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/EquipementRepository.java new file mode 100644 index 0000000..06cfea3 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/EquipementRepository.java @@ -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 { +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/LicenceRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/LicenceRepository.java new file mode 100644 index 0000000..d0a2a37 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/LicenceRepository.java @@ -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 { + List findByAdherentId(Long adherentId); + + long countByEtat(String etat); + + @Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c") + BigDecimal sumTarifBase(); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/ModePaiementRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/ModePaiementRepository.java new file mode 100644 index 0000000..b3cc8dc --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/ModePaiementRepository.java @@ -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 { + Optional findByNom(String nom); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/repository/PaiementRepository.java b/as-talange-core/src/main/java/com/astalange/core/repository/PaiementRepository.java new file mode 100644 index 0000000..03237b4 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/repository/PaiementRepository.java @@ -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 { + + @Query("SELECT SUM(p.montant) FROM Paiement p") + BigDecimal sumMontant(); +} diff --git a/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java b/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..f6be8a5 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/security/UserDetailsServiceImpl.java @@ -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()) + ); + } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/validation/RepresentantLegalValidator.java b/as-talange-core/src/main/java/com/astalange/core/validation/RepresentantLegalValidator.java new file mode 100644 index 0000000..beb2585 --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/validation/RepresentantLegalValidator.java @@ -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 { + + @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; + } +} diff --git a/as-talange-core/src/main/java/com/astalange/core/validation/ValidRepresentantLegal.java b/as-talange-core/src/main/java/com/astalange/core/validation/ValidRepresentantLegal.java new file mode 100644 index 0000000..3500a4e --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/validation/ValidRepresentantLegal.java @@ -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[] payload() default {}; +} diff --git a/as-talange-core/src/main/resources/db/migration/V1__init_schema.sql b/as-talange-core/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..c8a9ac8 --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V1__init_schema.sql @@ -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) +); diff --git a/as-talange-core/src/main/resources/db/migration/V2__add_mode_paiement.sql b/as-talange-core/src/main/resources/db/migration/V2__add_mode_paiement.sql new file mode 100644 index 0000000..ae8b25a --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V2__add_mode_paiement.sql @@ -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; diff --git a/as-talange-core/src/main/resources/db/migration/V3__insert_default_data.sql b/as-talange-core/src/main/resources/db/migration/V3__insert_default_data.sql new file mode 100644 index 0000000..7006bea --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V3__insert_default_data.sql @@ -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); diff --git a/as-talange-core/src/main/resources/db/migration/V4__add_residency_pricing.sql b/as-talange-core/src/main/resources/db/migration/V4__add_residency_pricing.sql new file mode 100644 index 0000000..d279275 --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V4__add_residency_pricing.sql @@ -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; diff --git a/as-talange-core/src/main/resources/db/migration/V5__add_numero_licence.sql b/as-talange-core/src/main/resources/db/migration/V5__add_numero_licence.sql new file mode 100644 index 0000000..29548b1 --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V5__add_numero_licence.sql @@ -0,0 +1 @@ +ALTER TABLE licence ADD COLUMN numero_licence VARCHAR(50); diff --git a/as-talange-core/src/test/java/com/astalange/core/HashGen.java b/as-talange-core/src/test/java/com/astalange/core/HashGen.java new file mode 100644 index 0000000..0491755 --- /dev/null +++ b/as-talange-core/src/test/java/com/astalange/core/HashGen.java @@ -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"); + } +} diff --git a/as-talange-web/pom.xml b/as-talange-web/pom.xml new file mode 100644 index 0000000..188cb56 --- /dev/null +++ b/as-talange-web/pom.xml @@ -0,0 +1,66 @@ + + + + as-talange-parent + com.astalange + 1.0-SNAPSHOT + + 4.0.0 + + as-talange-web + + + + com.astalange + as-talange-core + ${project.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.astalange.web.AsTalangeApplication + + + + + diff --git a/as-talange-web/src/main/java/com/astalange/web/AsTalangeApplication.java b/as-talange-web/src/main/java/com/astalange/web/AsTalangeApplication.java new file mode 100644 index 0000000..ff30c4f --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/AsTalangeApplication.java @@ -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); + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java b/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java new file mode 100644 index 0000000..5cfaa6f --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/config/SecurityConfig.java @@ -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; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java new file mode 100644 index 0000000..1489053 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/AdherentController.java @@ -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 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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java b/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java new file mode 100644 index 0000000..4237fca --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/CategorieController.java @@ -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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/HomeController.java b/as-talange-web/src/main/java/com/astalange/web/controller/HomeController.java new file mode 100644 index 0000000..0223e85 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/HomeController.java @@ -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 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 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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java b/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java new file mode 100644 index 0000000..dd20734 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/LicenceController.java @@ -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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/ModePaiementController.java b/as-talange-web/src/main/java/com/astalange/web/controller/ModePaiementController.java new file mode 100644 index 0000000..5bd7fe3 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/ModePaiementController.java @@ -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"; + } +} diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/PaiementController.java b/as-talange-web/src/main/java/com/astalange/web/controller/PaiementController.java new file mode 100644 index 0000000..b94d133 --- /dev/null +++ b/as-talange-web/src/main/java/com/astalange/web/controller/PaiementController.java @@ -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"; + } +} diff --git a/as-talange-web/src/main/resources/application.yml b/as-talange-web/src/main/resources/application.yml new file mode 100644 index 0000000..7082741 --- /dev/null +++ b/as-talange-web/src/main/resources/application.yml @@ -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 diff --git a/as-talange-web/src/main/resources/templates/adherents/form.html b/as-talange-web/src/main/resources/templates/adherents/form.html new file mode 100644 index 0000000..d80484b --- /dev/null +++ b/as-talange-web/src/main/resources/templates/adherents/form.html @@ -0,0 +1,328 @@ + + + + + Nouvel Adhérent - AS Talange + + + + + + + + + + + +
+ +
+

Ajouter un adhérent

+
+ + +
+
+ +
+ + +
+

+
+ +
+ +
+ + +

+
+ + +
+ + +

+
+
+ +
+ +
+ + +

+
+ + +
+ + +

+
+
+ + +
+ +

Obligatoire car l'adhérent a moins de 18 ans.

+ +

+
+ +
+ +
+ + +

+
+ + +
+ + +
+
+ + +
+ + +

+
+ +
+ Annuler + +
+
+
+ + +
+
+

Licences de l'adhérent

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SaisonCatégorieN° LicenceÉtatReste à PayerActions
Aucune licence enregistrée.
2024-2025U15- + Brouillon + 100.00 € + +
+
Historique des paiements
+
+ + 50 € + | + Chèque + | + 01/01/2026 + +
+
+
+ +
+
+ + + + + + + + + + diff --git a/as-talange-web/src/main/resources/templates/adherents/list.html b/as-talange-web/src/main/resources/templates/adherents/list.html new file mode 100644 index 0000000..aa226c4 --- /dev/null +++ b/as-talange-web/src/main/resources/templates/adherents/list.html @@ -0,0 +1,101 @@ + + + + + Adhérents - AS Talange + + + + + + + + + + + +
+ +
+

Gestion des Adhérents

+ +
+ + +
+
+

Liste des Adhérents

+ + + Nouvel Adhérent + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NomPrénomN° LicenceEmailTéléphonePaiementActions
Aucun adhérent enregistré.
DupontJean-jean@example.com0600000000 + À jour + + Voir / Modifier +
+ +
+
+
+
+
+ + + diff --git a/as-talange-web/src/main/resources/templates/index.html b/as-talange-web/src/main/resources/templates/index.html new file mode 100644 index 0000000..3f3aa9d --- /dev/null +++ b/as-talange-web/src/main/resources/templates/index.html @@ -0,0 +1,132 @@ + + + + + Tableau de bord - AS Talange + + + + + + + + + + + +
+ +
+

Tableau de bord

+
+ + +
+ + +
+ +
+
+ +
+
+

Total Adhérents

+

0

+
+
+ + +
+
+ +
+
+

Licences Actives

+

0

+

+
+
+ + +
+
+ +
+
+

Total Encaissé

+

0 €

+
+
+ + +
+
+ +
+
+

Reste à Recouvrer

+

0 €

+
+
+
+ + +
+
+

Derniers adhérents inscrits

+ Voir tout → +
+ + + + + + + + + + + + + + + + + + + + +
Nom & PrénomDate de naissanceTéléphoneAction
Aucun adhérent récent.
+
Dupont Jean
+
jean@example.com
+
01/01/20000600000000 + Voir profil +
+
+ +
+
+ + + diff --git a/as-talange-web/src/main/resources/templates/login.html b/as-talange-web/src/main/resources/templates/login.html new file mode 100644 index 0000000..cb76325 --- /dev/null +++ b/as-talange-web/src/main/resources/templates/login.html @@ -0,0 +1,46 @@ + + + + + Connexion - AS Talange + + + + + + +
+
+

AS Talange

+

Outil de gestion administrative

+
+ +
+ Identifiant ou mot de passe incorrect. +
+ +
+ Vous avez été déconnecté avec succès. +
+ +
+
+ + +
+ +
+ + +
+ + +
+
+ + + diff --git a/as-talange-web/src/main/resources/templates/parametrage/categories_form.html b/as-talange-web/src/main/resources/templates/parametrage/categories_form.html new file mode 100644 index 0000000..82a06ce --- /dev/null +++ b/as-talange-web/src/main/resources/templates/parametrage/categories_form.html @@ -0,0 +1,73 @@ + + + + + Catégorie - AS Talange + + + + + + + + +
+
+

Catégorie

+
+ +
+
+
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ Annuler + +
+
+
+
+
+ + diff --git a/as-talange-web/src/main/resources/templates/parametrage/categories_list.html b/as-talange-web/src/main/resources/templates/parametrage/categories_list.html new file mode 100644 index 0000000..a8935c1 --- /dev/null +++ b/as-talange-web/src/main/resources/templates/parametrage/categories_list.html @@ -0,0 +1,73 @@ + + + + + Paramétrage - Catégories + + + + + + + + +
+
+

Catégories Sportives

+
+ +
+
+

Liste des Catégories

+ + + Nouvelle Catégorie + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Nom de la catégorieAnnée MinAnnée MaxTarif (Talangeois)Tarif (Hors Commune)Actions
Aucune catégorie n'est paramétrée.
U1520102011100.00 €130.00 € + Modifier +
+ +
+
+
+
+
+ + diff --git a/as-talange-web/src/main/resources/templates/parametrage/modespaiement_form.html b/as-talange-web/src/main/resources/templates/parametrage/modespaiement_form.html new file mode 100644 index 0000000..814cf9e --- /dev/null +++ b/as-talange-web/src/main/resources/templates/parametrage/modespaiement_form.html @@ -0,0 +1,49 @@ + + + + + Mode de Paiement - AS Talange + + + + + + + + +
+
+

Mode de Paiement

+
+ +
+
+
+ + +
+ + +
+ +
+ Annuler + +
+
+
+
+
+ + diff --git a/as-talange-web/src/main/resources/templates/parametrage/modespaiement_list.html b/as-talange-web/src/main/resources/templates/parametrage/modespaiement_list.html new file mode 100644 index 0000000..f3c0d03 --- /dev/null +++ b/as-talange-web/src/main/resources/templates/parametrage/modespaiement_list.html @@ -0,0 +1,67 @@ + + + + + Paramétrage - Modes de Paiement + + + + + + + + +
+
+

Modes de Paiement

+
+ +
+
+

Liste des Modes de Paiement

+ + + Nouveau Mode + +
+ +
+ + + + + + + + + + + + + + + + + + +
IDNomActions
Aucun mode de paiement trouvé.
1Chèque + Éditer +
+ +
+
+
+
+
+ + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de26830 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7ff3936 --- /dev/null +++ b/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + + com.astalange + as-talange-parent + 1.0-SNAPSHOT + pom + + as-talange-parent + AS Talange Management Application + + + 21 + + + + as-talange-common + as-talange-core + as-talange-web + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d7de9a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +argon2-cffi==25.1.0 +argon2-cffi-bindings==25.1.0 +cffi==2.0.0 +pycparser==3.0