feat: implémentation de la gestion des saisons (UI, service, refactoring sidebar)

This commit is contained in:
2026-05-29 22:35:33 +02:00
parent dd31d73fed
commit a429e67133
27 changed files with 591 additions and 185 deletions
@@ -26,6 +26,10 @@ public class Categorie {
@Column(name = "tarif_exterieur", nullable = false, precision = 10, scale = 2) @Column(name = "tarif_exterieur", nullable = false, precision = 10, scale = 2)
private BigDecimal tarifExterieur; private BigDecimal tarifExterieur;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -45,4 +49,7 @@ public class Categorie {
public BigDecimal getTarifExterieur() { return tarifExterieur; } public BigDecimal getTarifExterieur() { return tarifExterieur; }
public void setTarifExterieur(BigDecimal tarifExterieur) { this.tarifExterieur = tarifExterieur; } public void setTarifExterieur(BigDecimal tarifExterieur) { this.tarifExterieur = tarifExterieur; }
public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; }
} }
@@ -23,6 +23,10 @@ public class Equipement {
@Column(name = "prix_contribution", nullable = false, precision = 10, scale = 2) @Column(name = "prix_contribution", nullable = false, precision = 10, scale = 2)
private BigDecimal prixContribution = BigDecimal.ZERO; private BigDecimal prixContribution = BigDecimal.ZERO;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -39,4 +43,7 @@ public class Equipement {
public BigDecimal getPrixContribution() { return prixContribution; } public BigDecimal getPrixContribution() { return prixContribution; }
public void setPrixContribution(BigDecimal prixContribution) { this.prixContribution = prixContribution; } public void setPrixContribution(BigDecimal prixContribution) { this.prixContribution = prixContribution; }
public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; }
} }
@@ -21,8 +21,9 @@ public class Licence {
@JoinColumn(name = "categorie_id", nullable = false) @JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie; private Categorie categorie;
@Column(nullable = false, length = 20) @ManyToOne(optional = false, fetch = FetchType.LAZY)
private String saison; @JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
@Column(nullable = false, length = 50) @Column(nullable = false, length = 50)
private String etat; private String etat;
@@ -89,8 +90,8 @@ public class Licence {
public Categorie getCategorie() { return categorie; } public Categorie getCategorie() { return categorie; }
public void setCategorie(Categorie categorie) { this.categorie = categorie; } public void setCategorie(Categorie categorie) { this.categorie = categorie; }
public String getSaison() { return saison; } public Saison getSaison() { return saison; }
public void setSaison(String saison) { this.saison = saison; } public void setSaison(Saison saison) { this.saison = saison; }
public String getEtat() { return etat; } public String getEtat() { return etat; }
public void setEtat(String etat) { this.etat = etat; } public void setEtat(String etat) { this.etat = etat; }
@@ -0,0 +1,58 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.util.Objects;
@Entity
@Table(name = "saison")
public class Saison {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nom;
@Column(name = "est_active", nullable = false)
private Boolean estActive = 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 Boolean getEstActive() {
return estActive;
}
public void setEstActive(Boolean estActive) {
this.estActive = estActive;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Saison saison = (Saison) o;
return Objects.equals(id, saison.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -9,4 +9,7 @@ import java.util.List;
@Repository @Repository
public interface AdherentRepository extends JpaRepository<Adherent, Long> { public interface AdherentRepository extends JpaRepository<Adherent, Long> {
List<Adherent> findTop5ByOrderByIdDesc(); List<Adherent> findTop5ByOrderByIdDesc();
@org.springframework.data.jpa.repository.Query("SELECT DISTINCT a FROM Adherent a LEFT JOIN a.licences l WHERE LOWER(a.nom) LIKE LOWER(CONCAT('%', :query, '%')) OR LOWER(a.prenom) LIKE LOWER(CONCAT('%', :query, '%')) OR LOWER(l.numeroLicence) LIKE LOWER(CONCAT('%', :query, '%'))")
List<Adherent> searchByQuery(@org.springframework.data.repository.query.Param("query") String query);
} }
@@ -4,6 +4,10 @@ import com.astalange.core.entity.Categorie;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.astalange.core.entity.Saison;
import java.util.List;
@Repository @Repository
public interface CategorieRepository extends JpaRepository<Categorie, Long> { public interface CategorieRepository extends JpaRepository<Categorie, Long> {
List<Categorie> findBySaison(Saison saison);
} }
@@ -4,6 +4,10 @@ import com.astalange.core.entity.Equipement;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.astalange.core.entity.Saison;
import java.util.List;
@Repository @Repository
public interface EquipementRepository extends JpaRepository<Equipement, Long> { public interface EquipementRepository extends JpaRepository<Equipement, Long> {
List<Equipement> findBySaison(Saison saison);
} }
@@ -0,0 +1,12 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Saison;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface SaisonRepository extends JpaRepository<Saison, Long> {
Optional<Saison> findByEstActiveTrue();
}
@@ -0,0 +1,84 @@
package com.astalange.core.service;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Equipement;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class SaisonService {
private final SaisonRepository saisonRepository;
private final CategorieRepository categorieRepository;
private final EquipementRepository equipementRepository;
public SaisonService(SaisonRepository saisonRepository, CategorieRepository categorieRepository, EquipementRepository equipementRepository) {
this.saisonRepository = saisonRepository;
this.categorieRepository = categorieRepository;
this.equipementRepository = equipementRepository;
}
@Transactional
public Saison creerNouvelleSaison(String nom, boolean importerParametragePrecedent) {
Optional<Saison> ancienneSaisonOpt = saisonRepository.findByEstActiveTrue();
// Désactiver toutes les saisons actuelles (il ne devrait y en avoir qu'une mais on s'assure de tout nettoyer)
List<Saison> toutesLesSaisons = saisonRepository.findAll();
for (Saison s : toutesLesSaisons) {
s.setEstActive(false);
saisonRepository.save(s);
}
// Création de la nouvelle saison
Saison nouvelleSaison = new Saison();
nouvelleSaison.setNom(nom);
nouvelleSaison.setEstActive(true);
nouvelleSaison = saisonRepository.save(nouvelleSaison);
if (importerParametragePrecedent && ancienneSaisonOpt.isPresent()) {
Saison ancienneSaison = ancienneSaisonOpt.get();
dupliquerParametrages(ancienneSaison, nouvelleSaison);
}
return nouvelleSaison;
}
private void dupliquerParametrages(Saison ancienneSaison, Saison nouvelleSaison) {
// 1. Dupliquer les équipements
List<Equipement> anciensEquipements = equipementRepository.findBySaison(ancienneSaison);
for (Equipement ancien : anciensEquipements) {
Equipement nouvelEquipement = new Equipement();
nouvelEquipement.setNom(ancien.getNom());
nouvelEquipement.setDescription(ancien.getDescription());
nouvelEquipement.setObligatoire(ancien.getObligatoire());
nouvelEquipement.setPrixContribution(ancien.getPrixContribution());
nouvelEquipement.setSaison(nouvelleSaison);
equipementRepository.save(nouvelEquipement);
}
// 2. Dupliquer les catégories (avec incrémentation de l'année)
List<Categorie> anciennesCategories = categorieRepository.findBySaison(ancienneSaison);
for (Categorie ancienne : anciennesCategories) {
Categorie nouvelleCategorie = new Categorie();
nouvelleCategorie.setNom(ancienne.getNom());
// Incrémentation de +1 sur l'année de naissance pivot
if (ancienne.getAnneeMin() != null) {
nouvelleCategorie.setAnneeMin(ancienne.getAnneeMin() + 1);
}
if (ancienne.getAnneeMax() != null) {
nouvelleCategorie.setAnneeMax(ancienne.getAnneeMax() + 1);
}
nouvelleCategorie.setTarifBase(ancienne.getTarifBase());
nouvelleCategorie.setTarifExterieur(ancienne.getTarifExterieur());
nouvelleCategorie.setSaison(nouvelleSaison);
categorieRepository.save(nouvelleCategorie);
}
}
}
@@ -0,0 +1,43 @@
-- V9__introduction_saison.sql
-- 1. Création de la table saison
CREATE TABLE saison (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL,
est_active BOOLEAN NOT NULL DEFAULT FALSE
);
-- Création d'une saison par défaut pour les données existantes
INSERT INTO saison (nom, est_active) VALUES ('Saison Actuelle', TRUE);
-- 2. Ajout de saison_id dans les tables de paramétrage
ALTER TABLE categorie ADD COLUMN saison_id BIGINT;
ALTER TABLE equipement ADD COLUMN saison_id BIGINT;
-- Il n'y a pas de table tarif, les tarifs sont dans categorie.
-- Affecter la saison par défaut aux paramètres existants
UPDATE categorie SET saison_id = (SELECT id FROM saison LIMIT 1);
UPDATE equipement SET saison_id = (SELECT id FROM saison LIMIT 1);
-- Rendre saison_id NOT NULL et ajouter les contraintes
ALTER TABLE categorie ALTER COLUMN saison_id SET NOT NULL;
ALTER TABLE categorie ADD CONSTRAINT fk_categorie_saison FOREIGN KEY (saison_id) REFERENCES saison(id);
ALTER TABLE equipement ALTER COLUMN saison_id SET NOT NULL;
ALTER TABLE equipement ADD CONSTRAINT fk_equipement_saison FOREIGN KEY (saison_id) REFERENCES saison(id);
-- 3. Ajout de saison_id sur licence
-- Licence a déjà une colonne saison (String). On va la remplacer par saison_id
ALTER TABLE licence RENAME COLUMN saison TO ancienne_saison;
ALTER TABLE licence ADD COLUMN saison_id BIGINT;
UPDATE licence SET saison_id = (SELECT id FROM saison LIMIT 1);
ALTER TABLE licence ALTER COLUMN saison_id SET NOT NULL;
ALTER TABLE licence ADD CONSTRAINT fk_licence_saison FOREIGN KEY (saison_id) REFERENCES saison(id);
ALTER TABLE licence DROP COLUMN ancienne_saison;
-- 4. Migration de paiement et dotation pour pointer sur licence_id au lieu de adherent_id
-- Ajout des colonnes licence_id
-- (Paiement et Dotation avaient un adherent_id qui n'est pas dans l'entité actuelle mais peut-être en base ?)
-- D'après les entités, Paiement et Dotation ont DEJA licence_id et n'ont PAS adherent_id !
-- Le plan proposait de modifier ça mais le code a DÉJÀ ces modifications.
-- Je vais juste vérifier s'il faut modifier la base de données.
-- Les entités Paiement.java et Dotation.java ont déjà `private Licence licence;`
@@ -1,8 +1,10 @@
package com.astalange.web.controller; package com.astalange.web.controller;
import com.astalange.core.entity.Adherent; import com.astalange.core.entity.Adherent;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.AdherentRepository; import com.astalange.core.repository.AdherentRepository;
import com.astalange.core.repository.CategorieRepository; import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,17 +20,25 @@ public class AdherentController {
private final CategorieRepository categorieRepository; private final CategorieRepository categorieRepository;
private final com.astalange.core.repository.LicenceRepository licenceRepository; private final com.astalange.core.repository.LicenceRepository licenceRepository;
private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository; private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository;
private final SaisonRepository saisonRepository;
public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository) { public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository) {
this.adherentRepository = adherentRepository; this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository; this.licenceRepository = licenceRepository;
this.modePaiementRepository = modePaiementRepository; this.modePaiementRepository = modePaiementRepository;
this.saisonRepository = saisonRepository;
} }
@GetMapping @GetMapping
public String listAdherents(Model model) { public String listAdherents(@org.springframework.web.bind.annotation.RequestParam(required = false) String query, Model model) {
List<Adherent> adherents = adherentRepository.findAll(); List<Adherent> adherents;
if (query != null && !query.trim().isEmpty()) {
adherents = adherentRepository.searchByQuery(query.trim());
model.addAttribute("searchQuery", query.trim());
} else {
adherents = adherentRepository.findAll();
}
model.addAttribute("adherents", adherents); model.addAttribute("adherents", adherents);
return "adherents/list"; return "adherents/list";
} }
@@ -57,8 +67,16 @@ public class AdherentController {
public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) { public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) {
Adherent adherent = adherentRepository.findById(id) Adherent adherent = adherentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id)); .orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("saisonActive", saisonActive);
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
model.addAttribute("adherent", adherent); model.addAttribute("adherent", adherent);
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("licences", licenceRepository.findByAdherentId(id)); model.addAttribute("licences", licenceRepository.findByAdherentId(id));
model.addAttribute("modesPaiement", modePaiementRepository.findAll()); model.addAttribute("modesPaiement", modePaiementRepository.findAll());
return "adherents/form"; return "adherents/form";
@@ -0,0 +1,14 @@
package com.astalange.web.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@ControllerAdvice
public class GlobalControllerAdvice {
@ModelAttribute("requestURI")
public String requestURI(final HttpServletRequest request) {
return request.getRequestURI();
}
}
@@ -5,11 +5,13 @@ import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Licence; import com.astalange.core.entity.Licence;
import com.astalange.core.entity.Dotation; import com.astalange.core.entity.Dotation;
import com.astalange.core.entity.Equipement; import com.astalange.core.entity.Equipement;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.AdherentRepository; import com.astalange.core.repository.AdherentRepository;
import com.astalange.core.repository.CategorieRepository; import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.LicenceRepository; import com.astalange.core.repository.LicenceRepository;
import com.astalange.core.repository.DotationRepository; import com.astalange.core.repository.DotationRepository;
import com.astalange.core.repository.EquipementRepository; import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -25,24 +27,26 @@ public class LicenceController {
private final LicenceRepository licenceRepository; private final LicenceRepository licenceRepository;
private final EquipementRepository equipementRepository; private final EquipementRepository equipementRepository;
private final DotationRepository dotationRepository; private final DotationRepository dotationRepository;
private final SaisonRepository saisonRepository;
public LicenceController(AdherentRepository adherentRepository, public LicenceController(AdherentRepository adherentRepository,
CategorieRepository categorieRepository, CategorieRepository categorieRepository,
LicenceRepository licenceRepository, LicenceRepository licenceRepository,
EquipementRepository equipementRepository, EquipementRepository equipementRepository,
DotationRepository dotationRepository) { DotationRepository dotationRepository,
SaisonRepository saisonRepository) {
this.adherentRepository = adherentRepository; this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository; this.licenceRepository = licenceRepository;
this.equipementRepository = equipementRepository; this.equipementRepository = equipementRepository;
this.dotationRepository = dotationRepository; this.dotationRepository = dotationRepository;
this.saisonRepository = saisonRepository;
} }
@PostMapping("/adherents/{id}/licences") @PostMapping("/adherents/{id}/licences")
public String addLicence( public String addLicence(
@PathVariable Long id, @PathVariable Long id,
@RequestParam Long categorieId, @RequestParam Long categorieId,
@RequestParam String saison,
@RequestParam String etat, @RequestParam String etat,
@RequestParam(required = false) String numeroLicence) { @RequestParam(required = false) String numeroLicence) {
@@ -56,18 +60,21 @@ public class LicenceController {
Categorie categorie = categorieRepository.findById(categorieId) Categorie categorie = categorieRepository.findById(categorieId)
.orElseThrow(() -> new IllegalArgumentException("Invalid Categorie ID")); .orElseThrow(() -> new IllegalArgumentException("Invalid Categorie ID"));
Saison saisonActive = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
Licence licence = new Licence(); Licence licence = new Licence();
licence.setAdherent(adherent); licence.setAdherent(adherent);
licence.setCategorie(categorie); licence.setCategorie(categorie);
licence.setSaison(saison); licence.setSaison(saisonActive);
licence.setEtat(etat); licence.setEtat(etat);
licence.setNumeroLicence(numeroLicence); licence.setNumeroLicence(numeroLicence);
licence = licenceRepository.save(licence); licence = licenceRepository.save(licence);
// Auto-initialize dotations for all configured equipments // Auto-initialize dotations for all configured equipments of the active season
List<Equipement> equipements = equipementRepository.findAll(); List<Equipement> equipements = equipementRepository.findBySaison(saisonActive);
for (Equipement eq : equipements) { for (Equipement eq : equipements) {
Dotation dotation = new Dotation(); Dotation dotation = new Dotation();
dotation.setLicence(licence); dotation.setLicence(licence);
@@ -82,4 +89,27 @@ public class LicenceController {
return "redirect:/adherents/" + id + "/edit"; return "redirect:/adherents/" + id + "/edit";
} }
@PostMapping("/adherents/{adherentId}/licences/{licenceId}/update")
public String updateLicence(
@PathVariable Long adherentId,
@PathVariable Long licenceId,
@RequestParam Long categorieId,
@RequestParam String etat,
@RequestParam(required = false) String numeroLicence) {
Licence licence = licenceRepository.findById(licenceId)
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
Categorie categorie = categorieRepository.findById(categorieId)
.orElseThrow(() -> new IllegalArgumentException("Invalid Categorie ID"));
licence.setCategorie(categorie);
licence.setEtat(etat);
licence.setNumeroLicence(numeroLicence);
licenceRepository.save(licence);
return "redirect:/adherents/" + adherentId + "/edit";
}
} }
@@ -0,0 +1,61 @@
package com.astalange.web.controller;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.service.SaisonService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/saisons")
public class SaisonController {
private final SaisonRepository saisonRepository;
private final SaisonService saisonService;
public SaisonController(SaisonRepository saisonRepository, SaisonService saisonService) {
this.saisonRepository = saisonRepository;
this.saisonService = saisonService;
}
@GetMapping
public String listSaisons(Model model) {
// Optionnel: On pourrait trier par ID descendant pour avoir la plus récente en haut
List<Saison> saisons = saisonRepository.findAll();
model.addAttribute("saisons", saisons);
return "parametrage/saisons_list";
}
@GetMapping("/new")
public String showCreateForm(Model model) {
return "parametrage/saison_form";
}
@PostMapping
public String saveSaison(@RequestParam String nom, @RequestParam(defaultValue = "false") boolean importerParametrage) {
saisonService.creerNouvelleSaison(nom, importerParametrage);
return "redirect:/saisons";
}
@PostMapping("/{id}/activer")
public String activerSaison(@PathVariable Long id) {
Saison saisonAActiver = saisonRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Saison introuvable : " + id));
// Désactiver toutes les saisons
List<Saison> toutes = saisonRepository.findAll();
for (Saison s : toutes) {
s.setEstActive(false);
saisonRepository.save(s);
}
// Activer la saison choisie
saisonAActiver.setEstActive(true);
saisonRepository.save(saisonAActiver);
return "redirect:/saisons";
}
}
@@ -14,26 +14,7 @@
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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="/paiements" 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Équipements</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 Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <main class="flex-1 flex flex-col h-screen overflow-hidden">
@@ -191,7 +172,7 @@
th:data-tarif-exterieur="${lic.categorie.tarifExterieur}" th:data-tarif-exterieur="${lic.categorie.tarifExterieur}"
th:data-equipements-contrib="${lic.getPrixTotal().subtract(adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur)}" th:data-equipements-contrib="${lic.getPrixTotal().subtract(adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur)}"
th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}"> th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}">
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td> <td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison.nom}">2024-2025</td>
<td class="py-4 px-4 text-gray-600"> <td class="py-4 px-4 text-gray-600">
<span th:text="${lic.categorie.nom}">U15</span> <span th:text="${lic.categorie.nom}">U15</span>
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span> <span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
@@ -206,9 +187,20 @@
<td class="py-4 px-4 font-semibold cell-reste-payer" <td class="py-4 px-4 font-semibold cell-reste-payer"
th:classappend="${lic.getResteAPayer() == 0 ? 'text-green-600' : 'text-red-600'}" th:classappend="${lic.getResteAPayer() == 0 ? 'text-green-600' : 'text-red-600'}"
th:text="${lic.getResteAPayer() + ' €'}">30.00 €</td> th:text="${lic.getResteAPayer() + ' €'}">30.00 €</td>
<td class="py-4 px-4 text-right"> <td class="py-4 px-4 text-right flex justify-end space-x-2">
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'" <button type="button"
th:data-licence-id="${lic.id}" class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg btn-payer">Payer</button> th:data-licence-id="${lic.id}"
th:data-categorie-id="${lic.categorie.id}"
th:data-numero-licence="${lic.numeroLicence}"
th:data-etat="${lic.etat}"
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'))"
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
<button th:if="${lic.getResteAPayer() > 0}"
type="button"
th:data-licence-id="${lic.id}"
th:data-reste-a-payer="${lic.getResteAPayer()}"
onclick="openPaiementModal(this.getAttribute('data-licence-id'), this.getAttribute('data-reste-a-payer'))"
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg btn-payer">Payer</button>
</td> </td>
</tr> </tr>
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50"> <tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
@@ -323,26 +315,29 @@
<div id="licenceModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50"> <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="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-xl bg-white">
<div class="mt-3 text-center"> <div class="mt-3 text-center">
<h3 class="text-lg leading-6 font-semibold text-gray-900 mb-4">Nouvelle Licence</h3> <h3 id="licenceModalTitle" 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"> <form id="licenceForm" th:if="${adherent.id != null}" th:action="@{/adherents/{id}/licences(id=${adherent.id})}" method="post" class="space-y-4 text-left">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label> <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"> <select id="categorieSelect" 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 value="">Sélectionnez une catégorie...</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option> <option th:each="cat : ${categories}" th:value="${cat.id}"
th:data-annee-min="${cat.anneeMin}"
th:data-annee-max="${cat.anneeMax}"
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label> <label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
<input type="text" name="numeroLicence" placeholder="Ex: FFF-123456" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"> <input id="numeroLicenceInput" type="text" name="numeroLicence" placeholder="Ex: FFF-123456" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Saison</label> <label class="block text-sm font-medium text-gray-700 mb-1">Saison</label>
<input 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"> <input id="saisonInput" type="text" name="saison" value="Saison Actuelle" readonly class="w-full border border-gray-200 bg-gray-50 rounded-lg px-4 py-2 text-sm text-gray-500 outline-none cursor-not-allowed">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">État</label> <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"> <select id="etatSelect" 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="Brouillon">Brouillon</option>
<option value="Validée">Validée</option> <option value="Validée">Validée</option>
<option value="Payée">Payée</option> <option value="Payée">Payée</option>
@@ -389,9 +384,47 @@
</div> </div>
</div> </div>
<script> <script th:inline="javascript">
function openLicenceModal() { function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon') {
document.getElementById('licenceModal').classList.remove('hidden'); document.getElementById('licenceModal').classList.remove('hidden');
const form = document.getElementById('licenceForm');
const title = document.getElementById('licenceModalTitle');
const adherentId = [[${adherent.id}]];
if (licenceId) {
title.textContent = 'Modifier la Licence';
form.action = '/adherents/' + adherentId + '/licences/' + licenceId + '/update';
document.getElementById('categorieSelect').value = categorieId;
document.getElementById('numeroLicenceInput').value = numeroLicence;
document.getElementById('etatSelect').value = etat;
} else {
title.textContent = 'Nouvelle Licence';
form.action = '/adherents/' + adherentId + '/licences';
document.getElementById('numeroLicenceInput').value = '';
document.getElementById('etatSelect').value = 'Brouillon';
// Pré-sélection de la catégorie selon l'année de naissance
const dateNaissanceStr = document.getElementById('dateNaissance').value;
const catSelect = document.getElementById('categorieSelect');
let foundCat = false;
if (dateNaissanceStr) {
const birthYear = new Date(dateNaissanceStr).getFullYear();
Array.from(catSelect.options).forEach(opt => {
const min = parseInt(opt.getAttribute('data-annee-min'));
const max = parseInt(opt.getAttribute('data-annee-max'));
if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) {
catSelect.value = opt.value;
foundCat = true;
}
});
}
if (!foundCat) {
catSelect.value = '';
}
}
} }
function closeLicenceModal() { function closeLicenceModal() {
document.getElementById('licenceModal').classList.add('hidden'); document.getElementById('licenceModal').classList.add('hidden');
@@ -13,26 +13,7 @@
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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="/paiements" 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Équipements</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 Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <main class="flex-1 flex flex-col h-screen overflow-hidden">
@@ -55,7 +36,12 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-4 border-b border-gray-200 flex justify-between items-center"> <div class="p-4 border-b border-gray-200 flex justify-between items-center">
<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"> <form th:action="@{/adherents}" method="get" class="flex items-center space-x-2">
<input type="text" name="query" th:value="${searchQuery}" placeholder="Nom, Prénom ou N° Licence..."
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">
<button type="submit" class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 border border-gray-300 transition-colors">Chercher</button>
<a th:if="${searchQuery != null and !searchQuery.isEmpty()}" th:href="@{/adherents}" class="text-sm text-gray-500 hover:text-gray-700 underline underline-offset-2 ml-2">Effacer</a>
</form>
</div> </div>
<table class="w-full text-left border-collapse"> <table class="w-full text-left border-collapse">
<thead> <thead>
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<aside th:fragment="sidebar" class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
<div class="h-16 flex items-center px-6 border-b border-gray-200">
<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="/" th:classappend="${requestURI == '/' ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Tableau de bord</a>
<a href="/adherents" th:classappend="${requestURI != null and requestURI.startsWith('/adherents') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Adhérents</a>
<a href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Paiements</a>
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Saisons</a>
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Catégories</a>
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Modes de Paiement</a>
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipements</a>
</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>
</body>
</html>
@@ -13,26 +13,7 @@
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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="/paiements" 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Équipements</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 Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <main class="flex-1 flex flex-col h-screen overflow-hidden">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">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 transition-colors">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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 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 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 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 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 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 transition-colors">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>
<a href="/equipements" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 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 transition-colors">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>
<a href="/equipements" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 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 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -9,20 +9,7 @@
</head> </head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden"> <body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex"> <div th:replace="~{fragments/sidebar :: sidebar}"></div>
<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>
<a href="/paiements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 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 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>
<a href="/equipements" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Équipements</a>
</nav>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden"> <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"> <header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Nouvelle Saison - 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 fragment -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
<h2 class="text-lg font-semibold text-gray-800">Ajouter une Saison</h2>
</header>
<div class="flex-1 overflow-auto p-6 flex justify-center items-start">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-8 w-full max-w-2xl mt-8">
<h3 class="text-xl font-bold text-gray-900 mb-6">Paramétrage de la nouvelle saison</h3>
<form th:action="@{/saisons}" method="post" class="space-y-6">
<!-- Nom de la saison -->
<div>
<label for="nom" class="block text-sm font-medium text-gray-700 mb-1">Nom de la saison *</label>
<input type="text" id="nom" name="nom" required placeholder="Ex: Saison 2025-2026"
class="w-full rounded-lg border-gray-300 border px-4 py-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Importer paramétrage -->
<div class="flex items-start bg-blue-50 p-4 rounded-lg border border-blue-100">
<div class="flex items-center h-5">
<input id="importerParametrage" name="importerParametrage" type="checkbox" value="true" checked
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="importerParametrage" class="font-medium text-blue-800">Dupliquer le paramétrage de la saison actuelle</label>
<p class="text-blue-600 mt-1">Si coché, toutes les catégories (avec +1 an sur l'âge pivot) et tous les équipements seront clonés automatiquement pour cette nouvelle saison.</p>
</div>
</div>
<!-- Note sur l'activation -->
<div class="p-4 bg-yellow-50 text-yellow-800 rounded-lg text-sm border border-yellow-200">
<strong>Attention :</strong> La création de cette saison la définira automatiquement comme la <strong>nouvelle saison active</strong> du club.
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-100">
<a th:href="@{/saisons}" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium">Annuler</a>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
Créer la saison
</button>
</div>
</form>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Paramétrage - Saisons</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 fragment -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
<h2 class="text-lg font-semibold text-gray-800">Saisons du Club</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 Saisons</h3>
<a th:href="@{/saisons/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
+ Nouvelle Saison
</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 Saison</th>
<th class="py-3 px-6 font-medium text-center">Statut</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(saisons)}">
<td colspan="3" class="py-8 text-center text-gray-500">Aucune saison configurée.</td>
</tr>
<tr th:each="saison : ${saisons}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900" th:text="${saison.nom}">Saison 2024-2025</td>
<td class="py-4 px-6 text-center">
<span th:if="${saison.estActive}" class="px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Saison Active
</span>
<span th:unless="${saison.estActive}" class="px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600">
Clôturée
</span>
</td>
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
<form th:if="${!saison.estActive}" th:action="@{/saisons/{id}/activer(id=${saison.id})}" method="post">
<button type="submit" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Définir Active</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>