feat: implement global pricing model, optional equipment contribution, and bulk save for dotations
This commit is contained in:
@@ -27,6 +27,12 @@ public class Dotation {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Boolean fourni = false;
|
private Boolean fourni = false;
|
||||||
|
|
||||||
|
@Column(length = 10)
|
||||||
|
private String numero;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean choisi = false;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -46,4 +52,10 @@ public class Dotation {
|
|||||||
|
|
||||||
public Boolean getFourni() { return fourni; }
|
public Boolean getFourni() { return fourni; }
|
||||||
public void setFourni(Boolean fourni) { this.fourni = fourni; }
|
public void setFourni(Boolean fourni) { this.fourni = fourni; }
|
||||||
|
|
||||||
|
public String getNumero() { return numero; }
|
||||||
|
public void setNumero(String numero) { this.numero = numero; }
|
||||||
|
|
||||||
|
public Boolean getChoisi() { return choisi; }
|
||||||
|
public void setChoisi(Boolean choisi) { this.choisi = choisi; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.astalange.core.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "educateur")
|
||||||
|
public class Educateur {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String nom;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String prenom;
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(length = 20)
|
||||||
|
private String telephone;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "categorie_id")
|
||||||
|
private Categorie categorie;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getNom() { return nom; }
|
||||||
|
public void setNom(String nom) { this.nom = nom; }
|
||||||
|
|
||||||
|
public String getPrenom() { return prenom; }
|
||||||
|
public void setPrenom(String prenom) { this.prenom = prenom; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getTelephone() { return telephone; }
|
||||||
|
public void setTelephone(String telephone) { this.telephone = telephone; }
|
||||||
|
|
||||||
|
public Categorie getCategorie() { return categorie; }
|
||||||
|
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.astalange.core.entity;
|
package com.astalange.core.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "equipement")
|
@Table(name = "equipement")
|
||||||
@@ -19,6 +20,9 @@ public class Equipement {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Boolean obligatoire = false;
|
private Boolean obligatoire = false;
|
||||||
|
|
||||||
|
@Column(name = "prix_contribution", nullable = false, precision = 10, scale = 2)
|
||||||
|
private BigDecimal prixContribution = BigDecimal.ZERO;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -32,4 +36,7 @@ public class Equipement {
|
|||||||
|
|
||||||
public Boolean getObligatoire() { return obligatoire; }
|
public Boolean getObligatoire() { return obligatoire; }
|
||||||
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
|
||||||
|
|
||||||
|
public BigDecimal getPrixContribution() { return prixContribution; }
|
||||||
|
public void setPrixContribution(BigDecimal prixContribution) { this.prixContribution = prixContribution; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ public class Licence {
|
|||||||
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<Dotation> dotations = new ArrayList<>();
|
private List<Dotation> dotations = new ArrayList<>();
|
||||||
|
|
||||||
// Logic for Reste A Payer
|
// Logic for Global Total Price
|
||||||
@Transient
|
@Transient
|
||||||
public BigDecimal getResteAPayer() {
|
public BigDecimal getPrixTotal() {
|
||||||
if (categorie == null) return BigDecimal.ZERO;
|
if (categorie == null) return BigDecimal.ZERO;
|
||||||
|
|
||||||
BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange())
|
BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange())
|
||||||
@@ -47,6 +47,25 @@ public class Licence {
|
|||||||
|
|
||||||
if (prixTotal == null) prixTotal = BigDecimal.ZERO;
|
if (prixTotal == null) prixTotal = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
if (dotations != null) {
|
||||||
|
for (Dotation dot : dotations) {
|
||||||
|
if (dot.getChoisi() && dot.getEquipement() != null) {
|
||||||
|
BigDecimal price = dot.getEquipement().getPrixContribution();
|
||||||
|
if (price != null) {
|
||||||
|
prixTotal = prixTotal.add(price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prixTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for Reste A Payer
|
||||||
|
@Transient
|
||||||
|
public BigDecimal getResteAPayer() {
|
||||||
|
BigDecimal prixTotal = getPrixTotal();
|
||||||
|
|
||||||
if (paiements == null || paiements.isEmpty()) {
|
if (paiements == null || paiements.isEmpty()) {
|
||||||
return prixTotal;
|
return prixTotal;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS numero VARCHAR(10);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS educateur (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
nom VARCHAR(100) NOT NULL,
|
||||||
|
prenom VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100),
|
||||||
|
telephone VARCHAR(20),
|
||||||
|
categorie_id BIGINT,
|
||||||
|
CONSTRAINT fk_educateur_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- Ajouter la colonne prix_contribution à la table equipement (avec valeur par défaut 0)
|
||||||
|
ALTER TABLE equipement ADD COLUMN IF NOT EXISTS prix_contribution DECIMAL(10,2) NOT NULL DEFAULT 0.00;
|
||||||
|
|
||||||
|
-- Ajouter la colonne choisi à la table dotation (avec valeur par défaut false)
|
||||||
|
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS choisi BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Mettre choisi à true pour tous les équipements obligatoires existants
|
||||||
|
UPDATE dotation d
|
||||||
|
SET choisi = TRUE
|
||||||
|
FROM equipement e
|
||||||
|
WHERE d.equipement_id = e.id AND e.obligatoire = TRUE;
|
||||||
|
|
||||||
|
-- Mettre à jour le prix de contribution du Survêtement à 30.00 € à titre d'exemple
|
||||||
|
UPDATE equipement SET prix_contribution = 30.00 WHERE nom = 'Survêtement';
|
||||||
@@ -3,10 +3,14 @@ package com.astalange.web.controller;
|
|||||||
import com.astalange.core.entity.Dotation;
|
import com.astalange.core.entity.Dotation;
|
||||||
import com.astalange.core.repository.DotationRepository;
|
import com.astalange.core.repository.DotationRepository;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class DotationController {
|
public class DotationController {
|
||||||
|
|
||||||
@@ -22,17 +26,61 @@ public class DotationController {
|
|||||||
@RequestParam Long adherentId,
|
@RequestParam Long adherentId,
|
||||||
@RequestParam(required = false, defaultValue = "") String taille,
|
@RequestParam(required = false, defaultValue = "") String taille,
|
||||||
@RequestParam(required = false, defaultValue = "") String flocage,
|
@RequestParam(required = false, defaultValue = "") String flocage,
|
||||||
@RequestParam(required = false) Boolean fourni) {
|
@RequestParam(required = false, defaultValue = "") String numero,
|
||||||
|
@RequestParam(required = false) Boolean fourni,
|
||||||
|
@RequestParam(required = false) Boolean choisi) {
|
||||||
|
|
||||||
Dotation dotation = dotationRepository.findById(id)
|
Dotation dotation = dotationRepository.findById(id)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
|
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
|
||||||
|
|
||||||
dotation.setTaille(taille);
|
dotation.setTaille(taille);
|
||||||
dotation.setFlocage(flocage);
|
dotation.setFlocage(flocage);
|
||||||
|
dotation.setNumero(numero);
|
||||||
dotation.setFourni(fourni != null && fourni);
|
dotation.setFourni(fourni != null && fourni);
|
||||||
|
|
||||||
|
if (dotation.getEquipement().getObligatoire()) {
|
||||||
|
dotation.setChoisi(true);
|
||||||
|
} else {
|
||||||
|
dotation.setChoisi(choisi != null && choisi);
|
||||||
|
}
|
||||||
|
|
||||||
dotationRepository.save(dotation);
|
dotationRepository.save(dotation);
|
||||||
|
|
||||||
return "redirect:/adherents/" + adherentId + "/edit";
|
return "redirect:/adherents/" + adherentId + "/edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@PostMapping("/dotations/bulk")
|
||||||
|
public String bulkUpdateDotations(
|
||||||
|
@RequestParam Long adherentId,
|
||||||
|
@RequestParam("dotationIds") List<Long> dotationIds,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
for (Long id : dotationIds) {
|
||||||
|
Dotation dotation = dotationRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
|
||||||
|
|
||||||
|
String taille = request.getParameter("taille_" + id);
|
||||||
|
String numero = request.getParameter("numero_" + id);
|
||||||
|
String flocage = request.getParameter("flocage_" + id);
|
||||||
|
String choisiParam = request.getParameter("choisi_" + id);
|
||||||
|
String fourniParam = request.getParameter("fourni_" + id);
|
||||||
|
|
||||||
|
if (taille != null) dotation.setTaille(taille);
|
||||||
|
if (numero != null) dotation.setNumero(numero);
|
||||||
|
if (flocage != null) dotation.setFlocage(flocage);
|
||||||
|
|
||||||
|
if (dotation.getEquipement().getObligatoire()) {
|
||||||
|
dotation.setChoisi(true);
|
||||||
|
} else {
|
||||||
|
dotation.setChoisi(choisiParam != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
dotation.setFourni(fourniParam != null);
|
||||||
|
|
||||||
|
dotationRepository.save(dotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "redirect:/adherents/" + adherentId + "/edit";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,7 @@ public class HomeController {
|
|||||||
|
|
||||||
List<com.astalange.core.entity.Licence> allLicences = licenceRepository.findAll();
|
List<com.astalange.core.entity.Licence> allLicences = licenceRepository.findAll();
|
||||||
BigDecimal totalAttendu = allLicences.stream()
|
BigDecimal totalAttendu = allLicences.stream()
|
||||||
.map(lic -> {
|
.map(com.astalange.core.entity.Licence::getPrixTotal)
|
||||||
if (lic.getCategorie() == null) return BigDecimal.ZERO;
|
|
||||||
return (lic.getAdherent() != null && lic.getAdherent().isResidentTalange())
|
|
||||||
? lic.getCategorie().getTarifBase()
|
|
||||||
: lic.getCategorie().getTarifExterieur();
|
|
||||||
})
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
BigDecimal resteARecouvrer = totalAttendu.subtract(totalEncaisse);
|
BigDecimal resteARecouvrer = totalAttendu.subtract(totalEncaisse);
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ public class LicenceController {
|
|||||||
dotation.setLicence(licence);
|
dotation.setLicence(licence);
|
||||||
dotation.setEquipement(eq);
|
dotation.setEquipement(eq);
|
||||||
dotation.setTaille("");
|
dotation.setTaille("");
|
||||||
dotation.setFlocage("");
|
dotation.setFlocage(adherent.getNom().toUpperCase() + " " + adherent.getPrenom());
|
||||||
|
dotation.setNumero("");
|
||||||
dotation.setFourni(false);
|
dotation.setFourni(false);
|
||||||
|
dotation.setChoisi(eq.getObligatoire());
|
||||||
dotationRepository.save(dotation);
|
dotationRepository.save(dotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,32 +156,39 @@
|
|||||||
<th class="py-3 px-4 font-medium">Catégorie</th>
|
<th class="py-3 px-4 font-medium">Catégorie</th>
|
||||||
<th class="py-3 px-4 font-medium">N° Licence</th>
|
<th class="py-3 px-4 font-medium">N° Licence</th>
|
||||||
<th class="py-3 px-4 font-medium">État</th>
|
<th class="py-3 px-4 font-medium">État</th>
|
||||||
|
<th class="py-3 px-4 font-medium">Tarif Global</th>
|
||||||
<th class="py-3 px-4 font-medium">Reste à Payer</th>
|
<th class="py-3 px-4 font-medium">Reste à Payer</th>
|
||||||
<th class="py-3 px-4 font-medium text-right">Actions</th>
|
<th class="py-3 px-4 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(licences)}">
|
<tr th:if="${#lists.isEmpty(licences)}">
|
||||||
<td colspan="6" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
|
<td colspan="7" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<th:block th:each="lic : ${licences}">
|
<th:block th:each="lic : ${licences}">
|
||||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||||
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td>
|
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td>
|
||||||
<td class="py-4 px-4 text-gray-600" th:text="${lic.categorie.nom}">U15</td>
|
<td class="py-4 px-4 text-gray-600">
|
||||||
|
<span th:text="${lic.categorie.nom}">U15</span>
|
||||||
|
<span class="text-[10px] text-gray-400 block" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
|
||||||
|
</td>
|
||||||
<td class="py-4 px-4 text-gray-600 font-mono text-xs" th:text="${lic.numeroLicence != null and !lic.numeroLicence.isEmpty() ? lic.numeroLicence : '-'}">-</td>
|
<td class="py-4 px-4 text-gray-600 font-mono text-xs" th:text="${lic.numeroLicence != null and !lic.numeroLicence.isEmpty() ? lic.numeroLicence : '-'}">-</td>
|
||||||
<td class="py-4 px-4">
|
<td class="py-4 px-4">
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||||
th:classappend="${lic.etat == 'Validée' ? 'bg-blue-100 text-blue-800' : (lic.etat == 'Payée' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800')}"
|
th:classappend="${lic.etat == 'Validée' ? 'bg-blue-100 text-blue-800' : (lic.etat == 'Payée' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800')}"
|
||||||
th:text="${lic.etat}">Brouillon</span>
|
th:text="${lic.etat}">Brouillon</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-4 text-gray-600 font-medium" th:text="${lic.getResteAPayer() + ' €'}">100.00 €</td>
|
<td class="py-4 px-4 text-gray-900 font-medium" th:text="${lic.getPrixTotal() + ' €'}">130.00 €</td>
|
||||||
|
<td class="py-4 px-4 font-semibold"
|
||||||
|
th:classappend="${lic.getResteAPayer() == 0 ? 'text-green-600' : 'text-red-600'}"
|
||||||
|
th:text="${lic.getResteAPayer() + ' €'}">30.00 €</td>
|
||||||
<td class="py-4 px-4 text-right">
|
<td class="py-4 px-4 text-right">
|
||||||
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'"
|
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'"
|
||||||
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">Payer</button>
|
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">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">
|
||||||
<td colspan="6" class="py-2 px-4">
|
<td colspan="7" class="py-2 px-4">
|
||||||
<div class="text-xs text-gray-500 mb-1 font-medium uppercase tracking-wider">Historique des paiements</div>
|
<div class="text-xs text-gray-500 mb-1 font-medium uppercase tracking-wider">Historique des paiements</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span th:each="paiement : ${lic.paiements}" class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-white border border-gray-200 text-gray-700">
|
<span th:each="paiement : ${lic.paiements}" class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-white border border-gray-200 text-gray-700">
|
||||||
@@ -195,48 +202,74 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="bg-gray-50/30">
|
<tr class="bg-gray-50/30">
|
||||||
<td colspan="6" class="py-3 px-4 border-t border-gray-100">
|
<td colspan="7" class="py-3 px-4 border-t border-gray-100">
|
||||||
<div class="text-xs text-gray-500 font-medium uppercase tracking-wider mb-2">Dotations Équipements</div>
|
<form th:action="@{/dotations/bulk}" method="post">
|
||||||
<div th:if="${#lists.isEmpty(lic.dotations)}" class="text-xs text-gray-500 italic">Aucune dotation pour cette licence.</div>
|
<input type="hidden" name="adherentId" th:value="${adherent.id}">
|
||||||
<div th:if="${!#lists.isEmpty(lic.dotations)}" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div th:each="dot : ${lic.dotations}" class="bg-white p-3 rounded-lg border border-gray-200 shadow-sm flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<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="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:text="${dot.equipement.obligatoire ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Formulaire pour mettre à jour la Dotation -->
|
<div class="flex justify-between items-center mb-3">
|
||||||
<form th:action="@{/dotations/{id}(id=${dot.id})}" method="post" class="space-y-2 pt-2 border-t border-gray-100">
|
<div class="text-xs text-gray-500 font-medium uppercase tracking-wider">Dotations Équipements</div>
|
||||||
<input type="hidden" name="adherentId" th:value="${adherent.id}">
|
<button th:if="${!#lists.isEmpty(lic.dotations)}" type="submit"
|
||||||
<div class="grid grid-cols-2 gap-1.5">
|
class="bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-1.5 rounded-lg font-semibold shadow-sm transition-colors flex items-center space-x-1">
|
||||||
<div>
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||||
<input type="text" name="taille" th:value="${dot.taille}" placeholder="S, M, L..."
|
</svg>
|
||||||
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
<span>Enregistrer les dotations</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div th:if="${#lists.isEmpty(lic.dotations)}" class="text-xs text-gray-500 italic">Aucune dotation pour cette licence.</div>
|
||||||
|
|
||||||
|
<div th:if="${!#lists.isEmpty(lic.dotations)}" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div th:each="dot : ${lic.dotations}" class="bg-white p-3 rounded-lg border border-gray-200 shadow-sm flex flex-col justify-between">
|
||||||
|
<input type="hidden" name="dotationIds" th:value="${dot.id}">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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="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:text="${dot.equipement.obligatoire ? 'Obligatoire' : 'Facultatif'}">Obligatoire</span>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Form inputs for bulk updating -->
|
||||||
|
<div class="space-y-2 pt-2 border-t border-gray-100">
|
||||||
|
<!-- 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}">
|
||||||
|
<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">
|
||||||
|
<span class="text-[10px] font-bold text-blue-800 uppercase tracking-wide" th:text="'Commander (+ ' + ${dot.equipement.prixContribution} + ' €)'">Commander (+ 30 €)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-1.5 mb-1.5">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[9px] uppercase font-bold text-gray-400">Taille</label>
|
||||||
|
<input type="text" th:name="'taille_' + ${dot.id}" th:value="${dot.taille}" placeholder="S, M, L..."
|
||||||
|
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[9px] uppercase font-bold text-gray-400">Numéro</label>
|
||||||
|
<input type="text" th:name="'numero_' + ${dot.id}" th:value="${dot.numero}" placeholder="Ex: 10"
|
||||||
|
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label>
|
<label class="block text-[9px] uppercase font-bold text-gray-400">Flocage</label>
|
||||||
<input type="text" name="flocage" th:value="${dot.flocage}" placeholder="N°/Nom"
|
<input type="text" th:name="'flocage_' + ${dot.id}" th:value="${dot.flocage}" placeholder="Nom du joueur"
|
||||||
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
class="w-full text-xs border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 outline-none">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center pt-1.5">
|
||||||
|
<label class="flex items-center space-x-1 cursor-pointer select-none">
|
||||||
|
<input type="checkbox" th:name="'fourni_' + ${dot.id}" th:checked="${dot.fourni}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||||
|
<span class="text-[11px] text-gray-700">Fourni</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center pt-1.5">
|
</div>
|
||||||
<label class="flex items-center space-x-1 cursor-pointer select-none">
|
|
||||||
<input type="checkbox" name="fourni" th:checked="${dot.fourni}" class="w-3.5 h-3.5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
|
||||||
<span class="text-[11px] text-gray-700">Fourni</span>
|
|
||||||
</label>
|
|
||||||
<button type="submit" class="bg-blue-50 text-blue-600 hover:bg-blue-100 text-[10px] px-2 py-0.5 rounded font-bold transition-colors">
|
|
||||||
Sauver
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</th:block>
|
</th:block>
|
||||||
@@ -257,7 +290,7 @@
|
|||||||
<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 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} + ' (' + ${cat.tarifBase} + ' €)'"></option>
|
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
<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">
|
<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">
|
<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">
|
<label for="obligatoire" class="text-sm font-medium text-gray-700">
|
||||||
|
|||||||
@@ -44,12 +44,13 @@
|
|||||||
<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">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="4" class="py-8 text-center text-gray-500">Aucun équipement n'est paramétré.</td>
|
<td colspan="5" 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>
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
th:classappend="${equip.obligatoire ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'}"
|
th:classappend="${equip.obligatoire ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'}"
|
||||||
th:text="${equip.obligatoire ? 'Oui' : 'Non'}">Oui</span>
|
th:text="${equip.obligatoire ? 'Oui' : 'Non'}">Oui</span>
|
||||||
</td>
|
</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