Refactor: Move equipment price and mandatory status to Categorie relationship
- Created CategorieEquipement entity to act as a join table between Category and Equipment. - Moved 'obligatoire' and 'prix' (formerly prixContribution) fields from Equipement to the new CategorieEquipement entity. - Added Flyway migrations V14 and V15 to update the database schema and migrate existing data. - Updated Categorie UI to use a select dropdown and price input for managing category-specific equipment configurations. - Replaced 'equipement.obligatoire' and 'equipement.prixContribution' calls with their new category-level equivalents in logic and templates. - Updated pricing calculation in Licence entity to source prices dynamically from the Categorie configuration. - Updated Dotation generation logic to sync with category requirements automatically.
This commit is contained in:
@@ -2,6 +2,8 @@ package com.astalange.core.entity;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "categorie")
|
@Table(name = "categorie")
|
||||||
@@ -30,6 +32,9 @@ public class Categorie {
|
|||||||
@JoinColumn(name = "saison_id", nullable = false)
|
@JoinColumn(name = "saison_id", nullable = false)
|
||||||
private Saison saison;
|
private Saison saison;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "categorie", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<CategorieEquipement> categorieEquipements = new ArrayList<>();
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -52,4 +57,29 @@ public class Categorie {
|
|||||||
|
|
||||||
public Saison getSaison() { return saison; }
|
public Saison getSaison() { return saison; }
|
||||||
public void setSaison(Saison saison) { this.saison = saison; }
|
public void setSaison(Saison saison) { this.saison = saison; }
|
||||||
|
|
||||||
|
public List<CategorieEquipement> getCategorieEquipements() { return categorieEquipements; }
|
||||||
|
public void setCategorieEquipements(List<CategorieEquipement> categorieEquipements) { this.categorieEquipements = categorieEquipements; }
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public boolean hasEquipement(Long equipementId) {
|
||||||
|
if (categorieEquipements == null) return false;
|
||||||
|
return categorieEquipements.stream().anyMatch(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public boolean isEquipementObligatoire(Long equipementId) {
|
||||||
|
if (categorieEquipements == null) return false;
|
||||||
|
return categorieEquipements.stream().anyMatch(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId) && Boolean.TRUE.equals(ce.getObligatoire()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public BigDecimal getEquipementPrix(Long equipementId) {
|
||||||
|
if (categorieEquipements == null) return BigDecimal.ZERO;
|
||||||
|
return categorieEquipements.stream()
|
||||||
|
.filter(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId))
|
||||||
|
.findFirst()
|
||||||
|
.map(CategorieEquipement::getPrix)
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.astalange.core.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "categorie_equipement")
|
||||||
|
public class CategorieEquipement {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "categorie_id", nullable = false)
|
||||||
|
private Categorie categorie;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "equipement_id", nullable = false)
|
||||||
|
private Equipement equipement;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean obligatoire = false;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private java.math.BigDecimal prix = java.math.BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
|
||||||
|
public java.math.BigDecimal getPrix() { return prix; }
|
||||||
|
public void setPrix(java.math.BigDecimal prix) { this.prix = prix; }
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Categorie getCategorie() { return categorie; }
|
||||||
|
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
||||||
|
|
||||||
|
public Equipement getEquipement() { return equipement; }
|
||||||
|
public void setEquipement(Equipement equipement) { this.equipement = equipement; }
|
||||||
|
|
||||||
|
public Boolean getObligatoire() { return obligatoire; }
|
||||||
|
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
||||||
|
}
|
||||||
@@ -17,11 +17,6 @@ public class Equipement {
|
|||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private Boolean obligatoire = false;
|
|
||||||
|
|
||||||
@Column(name = "prix_contribution", nullable = false, precision = 10, scale = 2)
|
|
||||||
private BigDecimal prixContribution = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "saison_id", nullable = false)
|
@JoinColumn(name = "saison_id", nullable = false)
|
||||||
@@ -38,12 +33,6 @@ public class Equipement {
|
|||||||
public String getDescription() { return description; }
|
public String getDescription() { return description; }
|
||||||
public void setDescription(String description) { this.description = description; }
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
public Boolean getObligatoire() { return obligatoire; }
|
|
||||||
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
|
||||||
|
|
||||||
public BigDecimal getPrixContribution() { return prixContribution; }
|
|
||||||
public void setPrixContribution(BigDecimal prixContribution) { this.prixContribution = prixContribution; }
|
|
||||||
|
|
||||||
public Saison getSaison() { return saison; }
|
public Saison getSaison() { return saison; }
|
||||||
public void setSaison(Saison saison) { this.saison = saison; }
|
public void setSaison(Saison saison) { this.saison = saison; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class Licence {
|
|||||||
if (dotations != null) {
|
if (dotations != null) {
|
||||||
for (Dotation dot : dotations) {
|
for (Dotation dot : dotations) {
|
||||||
if (dot.getChoisi() && dot.getEquipement() != null) {
|
if (dot.getChoisi() && dot.getEquipement() != null) {
|
||||||
BigDecimal price = dot.getEquipement().getPrixContribution();
|
BigDecimal price = categorie.getEquipementPrix(dot.getEquipement().getId());
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
prixTotal = prixTotal.add(price);
|
prixTotal = prixTotal.add(price);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import java.util.List;
|
|||||||
public interface LicenceRepository extends JpaRepository<Licence, Long> {
|
public interface LicenceRepository extends JpaRepository<Licence, Long> {
|
||||||
List<Licence> findByAdherentId(Long adherentId);
|
List<Licence> findByAdherentId(Long adherentId);
|
||||||
|
|
||||||
|
List<Licence> findBySaisonAndCategorie(com.astalange.core.entity.Saison saison, com.astalange.core.entity.Categorie categorie);
|
||||||
|
|
||||||
long countByEtat(String etat);
|
long countByEtat(String etat);
|
||||||
|
|
||||||
@Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c")
|
@Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c")
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.astalange.core.service;
|
||||||
|
|
||||||
|
import com.astalange.core.entity.*;
|
||||||
|
import com.astalange.core.repository.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CategorieService {
|
||||||
|
|
||||||
|
private final CategorieRepository categorieRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
private final EquipementRepository equipementRepository;
|
||||||
|
private final LicenceRepository licenceRepository;
|
||||||
|
|
||||||
|
public CategorieService(CategorieRepository categorieRepository, SaisonRepository saisonRepository, EquipementRepository equipementRepository, LicenceRepository licenceRepository) {
|
||||||
|
this.categorieRepository = categorieRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
|
this.equipementRepository = equipementRepository;
|
||||||
|
this.licenceRepository = licenceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EquipementConfig(boolean obligatoire, java.math.BigDecimal prix) {}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void saveCategorie(Categorie categorie, java.util.Map<Long, EquipementConfig> equipementConfigs) {
|
||||||
|
Saison activeSaison;
|
||||||
|
Categorie existing = null;
|
||||||
|
if (categorie.getId() == null) {
|
||||||
|
activeSaison = saisonRepository.findByEstActiveTrue()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
|
||||||
|
categorie.setSaison(activeSaison);
|
||||||
|
} else {
|
||||||
|
final Long catId = categorie.getId();
|
||||||
|
existing = categorieRepository.findById(catId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + catId));
|
||||||
|
categorie.setSaison(existing.getSaison());
|
||||||
|
activeSaison = existing.getSaison();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CategorieEquipement
|
||||||
|
if (existing != null) {
|
||||||
|
categorie.getCategorieEquipements().clear();
|
||||||
|
categorie.getCategorieEquipements().addAll(existing.getCategorieEquipements());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CategorieEquipement> nouveauxEquipements = new ArrayList<>();
|
||||||
|
if (equipementConfigs != null) {
|
||||||
|
for (java.util.Map.Entry<Long, EquipementConfig> entry : equipementConfigs.entrySet()) {
|
||||||
|
Long eqId = entry.getKey();
|
||||||
|
EquipementConfig config = entry.getValue();
|
||||||
|
Equipement eq = equipementRepository.findById(eqId).orElse(null);
|
||||||
|
if (eq != null) {
|
||||||
|
CategorieEquipement ce = new CategorieEquipement();
|
||||||
|
ce.setCategorie(categorie);
|
||||||
|
ce.setEquipement(eq);
|
||||||
|
ce.setObligatoire(config.obligatoire());
|
||||||
|
ce.setPrix(config.prix() != null ? config.prix() : java.math.BigDecimal.ZERO);
|
||||||
|
nouveauxEquipements.add(ce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categorie.getCategorieEquipements().clear();
|
||||||
|
categorie.getCategorieEquipements().addAll(nouveauxEquipements);
|
||||||
|
|
||||||
|
categorie = categorieRepository.save(categorie);
|
||||||
|
|
||||||
|
// Mettre à jour les adhérents (via les licences de la saison actuelle)
|
||||||
|
List<Licence> licences = licenceRepository.findBySaisonAndCategorie(activeSaison, categorie);
|
||||||
|
for (Licence licence : licences) {
|
||||||
|
updateDotationsForLicence(licence, nouveauxEquipements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDotationsForLicence(Licence licence, List<CategorieEquipement> categorieEquipements) {
|
||||||
|
List<Dotation> currentDotations = licence.getDotations();
|
||||||
|
List<Dotation> toRemove = new ArrayList<>();
|
||||||
|
|
||||||
|
// Remove dotations for equipements no longer in the category
|
||||||
|
for (Dotation d : currentDotations) {
|
||||||
|
boolean found = false;
|
||||||
|
for (CategorieEquipement ce : categorieEquipements) {
|
||||||
|
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
toRemove.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentDotations.removeAll(toRemove);
|
||||||
|
|
||||||
|
// Add missing dotations
|
||||||
|
for (CategorieEquipement ce : categorieEquipements) {
|
||||||
|
boolean found = false;
|
||||||
|
for (Dotation d : currentDotations) {
|
||||||
|
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
|
||||||
|
found = true;
|
||||||
|
if (Boolean.TRUE.equals(ce.getObligatoire()) && !Boolean.TRUE.equals(d.getChoisi())) {
|
||||||
|
d.setChoisi(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
Dotation newDotation = new Dotation();
|
||||||
|
newDotation.setLicence(licence);
|
||||||
|
newDotation.setEquipement(ce.getEquipement());
|
||||||
|
newDotation.setChoisi(ce.getObligatoire()); // By default checked if obligatoire
|
||||||
|
newDotation.setFourni(false);
|
||||||
|
currentDotations.add(newDotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
licenceRepository.save(licence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,14 +53,15 @@ public class SaisonService {
|
|||||||
private void dupliquerParametrages(Saison ancienneSaison, Saison nouvelleSaison) {
|
private void dupliquerParametrages(Saison ancienneSaison, Saison nouvelleSaison) {
|
||||||
// 1. Dupliquer les équipements
|
// 1. Dupliquer les équipements
|
||||||
List<Equipement> anciensEquipements = equipementRepository.findBySaison(ancienneSaison);
|
List<Equipement> anciensEquipements = equipementRepository.findBySaison(ancienneSaison);
|
||||||
|
java.util.Map<Long, Equipement> oldToNewEquipementMap = new java.util.HashMap<>();
|
||||||
|
|
||||||
for (Equipement ancien : anciensEquipements) {
|
for (Equipement ancien : anciensEquipements) {
|
||||||
Equipement nouvelEquipement = new Equipement();
|
Equipement nouvelEquipement = new Equipement();
|
||||||
nouvelEquipement.setNom(ancien.getNom());
|
nouvelEquipement.setNom(ancien.getNom());
|
||||||
nouvelEquipement.setDescription(ancien.getDescription());
|
nouvelEquipement.setDescription(ancien.getDescription());
|
||||||
nouvelEquipement.setObligatoire(ancien.getObligatoire());
|
|
||||||
nouvelEquipement.setPrixContribution(ancien.getPrixContribution());
|
|
||||||
nouvelEquipement.setSaison(nouvelleSaison);
|
nouvelEquipement.setSaison(nouvelleSaison);
|
||||||
equipementRepository.save(nouvelEquipement);
|
nouvelEquipement = equipementRepository.save(nouvelEquipement);
|
||||||
|
oldToNewEquipementMap.put(ancien.getId(), nouvelEquipement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Dupliquer les catégories (avec incrémentation de l'année)
|
// 2. Dupliquer les catégories (avec incrémentation de l'année)
|
||||||
@@ -78,6 +79,22 @@ public class SaisonService {
|
|||||||
nouvelleCategorie.setTarifBase(ancienne.getTarifBase());
|
nouvelleCategorie.setTarifBase(ancienne.getTarifBase());
|
||||||
nouvelleCategorie.setTarifExterieur(ancienne.getTarifExterieur());
|
nouvelleCategorie.setTarifExterieur(ancienne.getTarifExterieur());
|
||||||
nouvelleCategorie.setSaison(nouvelleSaison);
|
nouvelleCategorie.setSaison(nouvelleSaison);
|
||||||
|
|
||||||
|
// Dupliquer les relations avec les équipements
|
||||||
|
if (ancienne.getCategorieEquipements() != null) {
|
||||||
|
for (com.astalange.core.entity.CategorieEquipement oldCatEq : ancienne.getCategorieEquipements()) {
|
||||||
|
Equipement newEq = oldToNewEquipementMap.get(oldCatEq.getEquipement().getId());
|
||||||
|
if (newEq != null) {
|
||||||
|
com.astalange.core.entity.CategorieEquipement newCatEq = new com.astalange.core.entity.CategorieEquipement();
|
||||||
|
newCatEq.setCategorie(nouvelleCategorie);
|
||||||
|
newCatEq.setEquipement(newEq);
|
||||||
|
newCatEq.setObligatoire(oldCatEq.getObligatoire());
|
||||||
|
newCatEq.setPrix(oldCatEq.getPrix());
|
||||||
|
nouvelleCategorie.getCategorieEquipements().add(newCatEq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
categorieRepository.save(nouvelleCategorie);
|
categorieRepository.save(nouvelleCategorie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE categorie_equipement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
categorie_id BIGINT NOT NULL,
|
||||||
|
equipement_id BIGINT NOT NULL,
|
||||||
|
obligatoire BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT fk_cat_equip_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_cat_equip_equipement FOREIGN KEY (equipement_id) REFERENCES equipement(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrer les données existantes : associer tous les équipements d'une saison à toutes les catégories de la même saison
|
||||||
|
-- et transférer la valeur "obligatoire" de l'équipement vers la nouvelle table de liaison
|
||||||
|
INSERT INTO categorie_equipement (categorie_id, equipement_id, obligatoire)
|
||||||
|
SELECT c.id, e.id, e.obligatoire
|
||||||
|
FROM categorie c
|
||||||
|
JOIN equipement e ON c.saison_id = e.saison_id;
|
||||||
|
|
||||||
|
-- Supprimer la colonne "obligatoire" de la table équipement
|
||||||
|
ALTER TABLE equipement DROP COLUMN obligatoire;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE categorie_equipement ADD COLUMN prix NUMERIC(10,2) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Migrer les données existantes
|
||||||
|
UPDATE categorie_equipement ce
|
||||||
|
SET prix = e.prix_contribution
|
||||||
|
FROM equipement e
|
||||||
|
WHERE ce.equipement_id = e.id;
|
||||||
|
|
||||||
|
ALTER TABLE equipement DROP COLUMN IF EXISTS prix_contribution;
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
package com.astalange.web.controller;
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
import com.astalange.core.entity.Categorie;
|
import com.astalange.core.entity.Categorie;
|
||||||
|
import com.astalange.core.entity.Saison;
|
||||||
import com.astalange.core.repository.CategorieRepository;
|
import com.astalange.core.repository.CategorieRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
|
import com.astalange.core.repository.EquipementRepository;
|
||||||
|
import com.astalange.core.service.CategorieService;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/categories")
|
@RequestMapping("/categories")
|
||||||
public class CategorieController {
|
public class CategorieController {
|
||||||
|
|
||||||
private final CategorieRepository categorieRepository;
|
private final CategorieRepository categorieRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
private final EquipementRepository equipementRepository;
|
||||||
|
private final CategorieService categorieService;
|
||||||
|
|
||||||
public CategorieController(CategorieRepository categorieRepository) {
|
public CategorieController(CategorieRepository categorieRepository, SaisonRepository saisonRepository, EquipementRepository equipementRepository, CategorieService categorieService) {
|
||||||
this.categorieRepository = categorieRepository;
|
this.categorieRepository = categorieRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
|
this.equipementRepository = equipementRepository;
|
||||||
|
this.categorieService = categorieService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -25,6 +37,8 @@ public class CategorieController {
|
|||||||
@GetMapping("/new")
|
@GetMapping("/new")
|
||||||
public String showCreateForm(Model model) {
|
public String showCreateForm(Model model) {
|
||||||
model.addAttribute("categorie", new Categorie());
|
model.addAttribute("categorie", new Categorie());
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of());
|
||||||
return "parametrage/categories_form";
|
return "parametrage/categories_form";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,12 +47,37 @@ public class CategorieController {
|
|||||||
Categorie categorie = categorieRepository.findById(id)
|
Categorie categorie = categorieRepository.findById(id)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
|
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
|
||||||
model.addAttribute("categorie", categorie);
|
model.addAttribute("categorie", categorie);
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||||
|
model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of());
|
||||||
return "parametrage/categories_form";
|
return "parametrage/categories_form";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie) {
|
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie,
|
||||||
categorieRepository.save(categorie);
|
jakarta.servlet.http.HttpServletRequest request) {
|
||||||
|
|
||||||
|
java.util.Map<Long, com.astalange.core.service.CategorieService.EquipementConfig> configs = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
request.getParameterMap().forEach((key, values) -> {
|
||||||
|
if (key.startsWith("etatEquipement_") && values.length > 0) {
|
||||||
|
String idStr = key.substring("etatEquipement_".length());
|
||||||
|
try {
|
||||||
|
Long eqId = Long.parseLong(idStr);
|
||||||
|
String etat = values[0];
|
||||||
|
if (!"NON_LIE".equals(etat)) {
|
||||||
|
boolean obligatoire = "OBLIGATOIRE".equals(etat);
|
||||||
|
String prixStr = request.getParameter("prixEquipement_" + eqId);
|
||||||
|
java.math.BigDecimal prix = java.math.BigDecimal.ZERO;
|
||||||
|
if (prixStr != null && !prixStr.trim().isEmpty()) {
|
||||||
|
prix = new java.math.BigDecimal(prixStr.trim());
|
||||||
|
}
|
||||||
|
configs.put(eqId, new com.astalange.core.service.CategorieService.EquipementConfig(obligatoire, prix));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
categorieService.saveCategorie(categorie, configs);
|
||||||
return "redirect:/categories";
|
return "redirect:/categories";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class DotationController {
|
|||||||
dotation.setNumero(numero);
|
dotation.setNumero(numero);
|
||||||
dotation.setFourni(fourni != null && fourni);
|
dotation.setFourni(fourni != null && fourni);
|
||||||
|
|
||||||
if (dotation.getEquipement().getObligatoire()) {
|
if (dotation.getLicence().getCategorie().isEquipementObligatoire(dotation.getEquipement().getId())) {
|
||||||
dotation.setChoisi(true);
|
dotation.setChoisi(true);
|
||||||
} else {
|
} else {
|
||||||
dotation.setChoisi(choisi != null && choisi);
|
dotation.setChoisi(choisi != null && choisi);
|
||||||
@@ -70,7 +70,7 @@ public class DotationController {
|
|||||||
if (numero != null) dotation.setNumero(numero);
|
if (numero != null) dotation.setNumero(numero);
|
||||||
if (flocage != null) dotation.setFlocage(flocage);
|
if (flocage != null) dotation.setFlocage(flocage);
|
||||||
|
|
||||||
if (dotation.getEquipement().getObligatoire()) {
|
if (dotation.getLicence().getCategorie().isEquipementObligatoire(dotation.getEquipement().getId())) {
|
||||||
dotation.setChoisi(true);
|
dotation.setChoisi(true);
|
||||||
} else {
|
} else {
|
||||||
dotation.setChoisi(choisiParam != null);
|
dotation.setChoisi(choisiParam != null);
|
||||||
|
|||||||
+14
-3
@@ -1,7 +1,9 @@
|
|||||||
package com.astalange.web.controller;
|
package com.astalange.web.controller;
|
||||||
|
|
||||||
import com.astalange.core.entity.Equipement;
|
import com.astalange.core.entity.Equipement;
|
||||||
|
import com.astalange.core.entity.Saison;
|
||||||
import com.astalange.core.repository.EquipementRepository;
|
import com.astalange.core.repository.EquipementRepository;
|
||||||
|
import com.astalange.core.repository.SaisonRepository;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -11,9 +13,11 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class EquipementController {
|
public class EquipementController {
|
||||||
|
|
||||||
private final EquipementRepository equipementRepository;
|
private final EquipementRepository equipementRepository;
|
||||||
|
private final SaisonRepository saisonRepository;
|
||||||
|
|
||||||
public EquipementController(EquipementRepository equipementRepository) {
|
public EquipementController(EquipementRepository equipementRepository, SaisonRepository saisonRepository) {
|
||||||
this.equipementRepository = equipementRepository;
|
this.equipementRepository = equipementRepository;
|
||||||
|
this.saisonRepository = saisonRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -38,8 +42,15 @@ public class EquipementController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public String saveEquipement(@ModelAttribute("equipement") Equipement equipement) {
|
public String saveEquipement(@ModelAttribute("equipement") Equipement equipement) {
|
||||||
if (equipement.getObligatoire() == null) {
|
|
||||||
equipement.setObligatoire(false);
|
if (equipement.getId() == null) {
|
||||||
|
Saison activeSaison = saisonRepository.findByEstActiveTrue()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
|
||||||
|
equipement.setSaison(activeSaison);
|
||||||
|
} else {
|
||||||
|
Equipement existing = equipementRepository.findById(equipement.getId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + equipement.getId()));
|
||||||
|
equipement.setSaison(existing.getSaison());
|
||||||
}
|
}
|
||||||
equipementRepository.save(equipement);
|
equipementRepository.save(equipement);
|
||||||
return "redirect:/equipements";
|
return "redirect:/equipements";
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@Transactional
|
||||||
public class LicenceController {
|
public class LicenceController {
|
||||||
|
|
||||||
private final AdherentRepository adherentRepository;
|
private final AdherentRepository adherentRepository;
|
||||||
@@ -73,18 +75,20 @@ public class LicenceController {
|
|||||||
|
|
||||||
licence = licenceRepository.save(licence);
|
licence = licenceRepository.save(licence);
|
||||||
|
|
||||||
// Auto-initialize dotations for all configured equipments of the active season
|
// Auto-initialize dotations for all configured equipments of the category
|
||||||
List<Equipement> equipements = equipementRepository.findBySaison(saisonActive);
|
List<CategorieEquipement> catEquipements = categorie.getCategorieEquipements();
|
||||||
for (Equipement eq : equipements) {
|
if (catEquipements != null) {
|
||||||
Dotation dotation = new Dotation();
|
for (CategorieEquipement ce : catEquipements) {
|
||||||
dotation.setLicence(licence);
|
Dotation dotation = new Dotation();
|
||||||
dotation.setEquipement(eq);
|
dotation.setLicence(licence);
|
||||||
dotation.setTaille("");
|
dotation.setEquipement(ce.getEquipement());
|
||||||
dotation.setFlocage(adherent.getNom().toUpperCase() + " " + adherent.getPrenom());
|
dotation.setTaille("");
|
||||||
dotation.setNumero("");
|
dotation.setFlocage(adherent.getNom().toUpperCase() + " " + adherent.getPrenom());
|
||||||
dotation.setFourni(false);
|
dotation.setNumero("");
|
||||||
dotation.setChoisi(eq.getObligatoire());
|
dotation.setFourni(false);
|
||||||
dotationRepository.save(dotation);
|
dotation.setChoisi(Boolean.TRUE.equals(ce.getObligatoire()));
|
||||||
|
dotationRepository.save(dotation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/adherents/" + id + "/edit";
|
return "redirect:/adherents/" + id + "/edit";
|
||||||
|
|||||||
@@ -282,8 +282,8 @@
|
|||||||
<div class="flex justify-between items-start mb-1 gap-1">
|
<div class="flex justify-between items-start mb-1 gap-1">
|
||||||
<span class="font-semibold text-xs text-gray-800 truncate" th:text="${dot.equipement.nom}">Maillot</span>
|
<span class="font-semibold text-xs text-gray-800 truncate" th:text="${dot.equipement.nom}">Maillot</span>
|
||||||
<span class="text-[9px] px-1.5 py-0.5 rounded-full font-bold flex-shrink-0"
|
<span class="text-[9px] px-1.5 py-0.5 rounded-full font-bold flex-shrink-0"
|
||||||
th:classappend="${dot.equipement.obligatoire ? 'bg-red-50 text-red-700 border border-red-100' : 'bg-gray-50 text-gray-600 border border-gray-100'}"
|
th:classappend="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'bg-red-50 text-red-700 border border-red-100' : 'bg-gray-50 text-gray-600 border border-gray-100'}"
|
||||||
th:text="${dot.equipement.obligatoire ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
|
th:text="${dot.licence.categorie.isEquipementObligatoire(dot.equipement.id) ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-gray-400 mb-2 truncate" th:title="${dot.equipement.description}" th:text="${dot.equipement.description != null and !dot.equipement.description.isEmpty() ? dot.equipement.description : 'Pas de description'}">Description</div>
|
<div class="text-[10px] text-gray-400 mb-2 truncate" th:title="${dot.equipement.description}" th:text="${dot.equipement.description != null and !dot.equipement.description.isEmpty() ? dot.equipement.description : 'Pas de description'}">Description</div>
|
||||||
<!-- Special dynamic label for equipment style -->
|
<!-- Special dynamic label for equipment style -->
|
||||||
@@ -306,10 +306,10 @@
|
|||||||
<!-- Form inputs for bulk updating -->
|
<!-- Form inputs for bulk updating -->
|
||||||
<div class="space-y-2 pt-2 border-t border-gray-100">
|
<div class="space-y-2 pt-2 border-t border-gray-100">
|
||||||
<!-- Choisi checkbox for optional equipment -->
|
<!-- Choisi checkbox for optional equipment -->
|
||||||
<div class="flex items-center mb-1.5 bg-blue-50/50 p-1.5 rounded border border-blue-100/50" th:if="${!dot.equipement.obligatoire}">
|
<div class="flex items-center mb-1.5 bg-blue-50/50 p-1.5 rounded border border-blue-100/50" th:if="${!dot.licence.categorie.isEquipementObligatoire(dot.equipement.id)}">
|
||||||
<label class="flex items-center space-x-1.5 cursor-pointer select-none">
|
<label class="flex items-center space-x-1.5 cursor-pointer select-none">
|
||||||
<input type="checkbox" th:name="'choisi_' + ${dot.id}" th:checked="${dot.choisi}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
<input type="checkbox" th:name="'choisi_' + ${dot.id}" th:checked="${dot.choisi}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||||
<span class="text-[10px] font-bold text-blue-800 uppercase tracking-wide" th:text="'Commander (+ ' + ${dot.equipement.prixContribution} + ' €)'">Commander (+ 30 €)</span>
|
<span class="text-[10px] font-bold text-blue-800 uppercase tracking-wide" th:text="'Commander (+ ' + ${dot.licence.categorie.getEquipementPrix(dot.equipement.id)} + ' €)'">Commander (+ 30 €)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 border-t border-gray-100">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Équipements liés à la catégorie</label>
|
||||||
|
<div class="space-y-3 max-h-64 overflow-y-auto">
|
||||||
|
<th:block th:each="equi : ${allEquipements}">
|
||||||
|
<div class="flex items-center space-x-4 bg-gray-50 p-2 rounded-lg border border-gray-100">
|
||||||
|
<div class="w-1/3">
|
||||||
|
<span class="block text-sm font-medium text-gray-900" th:text="${equi.nom}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/3">
|
||||||
|
<select th:name="'etatEquipement_' + ${equi.id}" class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
<option value="NON_LIE" th:selected="${!categorie.hasEquipement(equi.id)}">Non lié</option>
|
||||||
|
<option value="OBLIGATOIRE" th:selected="${categorie.hasEquipement(equi.id) and categorie.isEquipementObligatoire(equi.id)}">Obligatoire (inclus)</option>
|
||||||
|
<option value="OPTIONNEL" th:selected="${categorie.hasEquipement(equi.id) and !categorie.isEquipementObligatoire(equi.id)}">Optionnel (payant)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/3 flex items-center space-x-2">
|
||||||
|
<label class="text-xs text-gray-500">Prix (€)</label>
|
||||||
|
<input type="number" step="0.01" th:name="'prixEquipement_' + ${equi.id}"
|
||||||
|
th:value="${categorie.getEquipementPrix(equi.id)}"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||||
<a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
<a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
||||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||||
|
|||||||
@@ -31,18 +31,8 @@
|
|||||||
<textarea th:field="*{description}" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
|
<textarea th:field="*{description}" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Prix de contribution (€) - Pour équipement facultatif</label>
|
|
||||||
<input type="number" step="0.01" th:field="*{prixContribution}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
|
||||||
<p class="text-xs text-gray-500 mt-1">Si l'équipement est facultatif, ce montant sera ajouté au tarif global de la licence s'il est commandé.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<input type="checkbox" id="obligatoire" th:field="*{obligatoire}" class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
|
||||||
<label for="obligatoire" class="text-sm font-medium text-gray-700">
|
|
||||||
Cet équipement est obligatoire pour tous les licenciés (inclus dans la licence)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||||
<a th:href="@{/equipements}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
<a th:href="@{/equipements}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
||||||
|
|||||||
@@ -30,24 +30,16 @@
|
|||||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||||
<th class="py-3 px-6 font-medium text-left">Nom de l'équipement</th>
|
<th class="py-3 px-6 font-medium text-left">Nom de l'équipement</th>
|
||||||
<th class="py-3 px-6 font-medium text-left">Description</th>
|
<th class="py-3 px-6 font-medium text-left">Description</th>
|
||||||
<th class="py-3 px-6 font-medium text-center">Obligatoire</th>
|
|
||||||
<th class="py-3 px-6 font-medium text-center">Prix Contribution</th>
|
|
||||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 text-sm">
|
<tbody class="divide-y divide-gray-200 text-sm">
|
||||||
<tr th:if="${#lists.isEmpty(equipements)}">
|
<tr th:if="${#lists.isEmpty(equipements)}">
|
||||||
<td colspan="5" class="py-8 text-center text-gray-500">Aucun équipement n'est paramétré.</td>
|
<td colspan="3" class="py-8 text-center text-gray-500">Aucun équipement n'est paramétré.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:each="equip : ${equipements}" class="hover:bg-gray-50 transition-colors">
|
<tr th:each="equip : ${equipements}" class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${equip.nom}">Maillot</td>
|
<td class="py-4 px-6 font-medium text-gray-900" th:text="${equip.nom}">Maillot</td>
|
||||||
<td class="py-4 px-6 text-gray-600" th:text="${equip.description != null ? equip.description : '-'}">Maillot de match officiel</td>
|
<td class="py-4 px-6 text-gray-600" th:text="${equip.description != null ? equip.description : '-'}">Maillot de match officiel</td>
|
||||||
<td class="py-4 px-6 text-center">
|
|
||||||
<span class="px-2.5 py-1 text-xs font-semibold rounded-full"
|
|
||||||
th:classappend="${equip.obligatoire ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'}"
|
|
||||||
th:text="${equip.obligatoire ? 'Oui' : 'Non'}">Oui</span>
|
|
||||||
</td>
|
|
||||||
<td class="py-4 px-6 text-center font-medium text-gray-700" th:text="${equip.obligatoire ? 'Inclus' : (equip.prixContribution + ' €')}">30 €</td>
|
|
||||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||||
<a th:href="@{/equipements/{id}/edit(id=${equip.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
<a th:href="@{/equipements/{id}/edit(id=${equip.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
||||||
<form th:action="@{/equipements/{id}/delete(id=${equip.id})}" method="post" onsubmit="return confirm('Supprimer cet équipement ? Cela supprimera également les dotations associées de toutes les licences.');">
|
<form th:action="@{/equipements/{id}/delete(id=${equip.id})}" method="post" onsubmit="return confirm('Supprimer cet équipement ? Cela supprimera également les dotations associées de toutes les licences.');">
|
||||||
|
|||||||
Reference in New Issue
Block a user