From 7c4addd52530a7267dd918ff8ac3b148ad0e1fdf Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 18 Dec 2025 14:46:52 +0100 Subject: [PATCH] feat: add cat filter on mass license validation --- .../ffsaf/domain/service/LicenceService.java | 20 ++++++ .../ffsaf/domain/service/MembreService.java | 12 ++-- .../ffsaf/rest/LicenceEndpoints.java | 17 ++++- .../webapp/src/pages/PayAndValidateList.jsx | 63 ++++++++++++++++--- .../pages/admin/member/InformationForm.jsx | 6 +- 5 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java index 751df30..f7cd1ce 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java @@ -84,6 +84,26 @@ public class LicenceService { )); } + public Uni validePaymentLicences(List ids) { + Uni uni = Uni.createFrom().nullItem(); + + for (Long id : ids) { + uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult() + .chain(model -> { + if (!model.isPay()) + ls.logUpdate("payment (admin) de la licence", model); + return validatePayLicences(model); + })) + .map(__ -> "OK"); + } + return uni.call(__ -> ls.append()); + } + + protected Uni validatePayLicences(LicenceModel model) { + model.setPay(true); + return Panache.withTransaction(() -> repository.persist(model)); + } + public Uni setLicence(long id, LicenceForm form) { if (form.getId() == -1) { return combRepository.findById(id).chain(membreModel -> { diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 7011e37..adead5e 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -287,14 +287,14 @@ public class MembreService { if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { LOGGER.info("Similar membres found: " + model); - throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); + throw new DBadRequestException("Email '" + model.getEmail() + "' déja utilisé"); } if (StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { LOGGER.info("Similar membres found: " + model); - throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); + throw new DBadRequestException("Email '" + model.getEmail() + "' déja utilisé"); } } @@ -380,7 +380,7 @@ public class MembreService { .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { if (c > 0 && !membre.getEmail().isBlank()) - throw new DBadRequestException("Email déjà utiliser"); + throw new DBadRequestException("Email déjà utilisé"); }))) .chain(membreModel -> clubRepository.findById(membre.getClub()) .map(club -> new Pair<>(membreModel, club))) @@ -402,7 +402,7 @@ public class MembreService { .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { if (c > 0 && !membre.getEmail().isBlank()) - throw new DBadRequestException("Email déjà utiliser"); + throw new DBadRequestException("Email déjà utilisé"); }))) .invoke(Unchecked.consumer(membreModel -> { if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) @@ -492,7 +492,7 @@ public class MembreService { return clubRepository.findById(input.getClub()) .call(__ -> repository.count("email LIKE ?1", input.getEmail()) .invoke(Unchecked.consumer(c -> { - if (c > 0) throw new DBadRequestException("Email déjà utiliser"); + if (c > 0) throw new DBadRequestException("Email déjà utilisé"); }))) .chain(clubModel -> { MembreModel model = getMembreModel(input, clubModel); @@ -508,7 +508,7 @@ public class MembreService { return repository.find("userId = ?1", subject).firstResult() .call(__ -> repository.count("email LIKE ?1", input.getEmail()) .invoke(Unchecked.consumer(c -> { - if (c > 0) throw new DBadRequestException("Email déjà utiliser"); + if (c > 0) throw new DBadRequestException("Email déjà utilisé"); }))) .call(membreModel -> repository.count( diff --git a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java index 9264b2d..4ba683e 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java @@ -118,7 +118,7 @@ public class LicenceEndpoints { @RolesAllowed("federation_admin") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @Operation(summary = "Validation licence", description = "Valide en masse les licence de l'année en cours (pour les administrateurs)") + @Operation(summary = "Validation licence", description = "Valide en masse les licences de l'année en cours (pour les administrateurs)") @APIResponses(value = { @APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"), @APIResponse(responseCode = "403", description = "Accès refusé"), @@ -128,6 +128,21 @@ public class LicenceEndpoints { return licenceService.valideLicences(ids); } + @POST + @Path("validate-pay") + @RolesAllowed("federation_admin") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Validation du payment des licences", description = "Valide en masse le payment des licences de l'année en cours (pour les administrateurs)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni validePaymentLicences(@Parameter(description = "Id des membres a valider") List ids) { + return licenceService.validePaymentLicences(ids); + } + @DELETE @Path("{id}") @RolesAllowed("federation_admin") diff --git a/src/main/webapp/src/pages/PayAndValidateList.jsx b/src/main/webapp/src/pages/PayAndValidateList.jsx index 6e0cc66..b81092b 100644 --- a/src/main/webapp/src/pages/PayAndValidateList.jsx +++ b/src/main/webapp/src/pages/PayAndValidateList.jsx @@ -4,13 +4,14 @@ import {AxiosError} from "../components/AxiosError.jsx"; import {ThreeDots} from "react-loader-spinner"; import {useEffect, useRef, useState} from "react"; import {useLocation, useNavigate} from "react-router-dom"; -import {apiAxios, errFormater} from "../utils/Tools.js"; +import {apiAxios, errFormater, getCatName} from "../utils/Tools.js"; import {toast} from "react-toastify"; import {SearchBar} from "../components/SearchBar.jsx"; import {ConfirmDialog} from "../components/ConfirmDialog.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons"; import "./PayAndValidateList.css"; +import * as Tools from "../utils/Tools.js"; export function PayAndValidateList({source}) { @@ -22,6 +23,7 @@ export function PayAndValidateList({source}) { const [memberData, setMemberData] = useState([]); const [licenceData, setLicenceData] = useState([]); const [clubFilter, setClubFilter] = useState(""); + const [catFilter, setCatFilter] = useState(""); const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2) const [lastSearch, setLastSearch] = useState(""); const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2); @@ -34,15 +36,15 @@ export function PayAndValidateList({source}) { data, error, refresh - } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1) + } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1) useEffect(() => { sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers)); }, [selectedMembers]); useEffect(() => { - refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`); - }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]); + refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`); + }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]); useEffect(() => { if (!data) @@ -62,7 +64,7 @@ export function PayAndValidateList({source}) { setMemberData(data2); }, [data, licenceData]); - useEffect(() => { + const fetchLicenceData = () => { toast.promise( apiAxios.get(`/licence/current/${source}`), { @@ -77,6 +79,10 @@ export function PayAndValidateList({source}) { .then(data => { setLicenceData(data.data); }); + }; + + useEffect(() => { + fetchLicenceData(); }, []); const search = (search) => { @@ -104,7 +110,31 @@ export function PayAndValidateList({source}) { } ).then(() => { setSelectedMembers([]); - refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`); + refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`); + }); + } + + const handlePayment = () => { + if (selectedMembers.length === 0) { + toast.error("Aucun membre sélectionné"); + return; + } + + toast.promise( + apiAxios.post(`/licence/validate-pay`, selectedMembers), + { + pending: "Validation des licences en cours...", + success: "Licences validées avec succès 🎉", + error: { + render({data}) { + return errFormater(data, "Échec de la validation des licences") + } + } + } + ).then(() => { + setSelectedMembers([]); + fetchLicenceData(); + refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`); }); } @@ -153,13 +183,20 @@ export function PayAndValidateList({source}) {
+ setPaymentFilter={setPaymentFilter} setCatFilter={setCatFilter} catFilter={catFilter}/>
{source === "admin" && <> - + + + { if (!data) return; @@ -395,6 +432,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta return
{source !== "club" && } +
+ +