Compare commits

...

2 Commits

Author SHA1 Message Date
ecc9753237 Merge pull request 'feat: add cat filter on mass license validation' (#69) from dev into master
Reviewed-on: #69
2025-12-18 13:47:32 +00:00
7c4addd525 feat: add cat filter on mass license validation
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-12-18 14:46:52 +01:00
5 changed files with 99 additions and 19 deletions

View File

@ -84,6 +84,26 @@ public class LicenceService {
)); ));
} }
public Uni<?> validePaymentLicences(List<Long> ids) {
Uni<String> 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<LicenceModel> validatePayLicences(LicenceModel model) {
model.setPay(true);
return Panache.withTransaction(() -> repository.persist(model));
}
public Uni<LicenceModel> setLicence(long id, LicenceForm form) { public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
if (form.getId() == -1) { if (form.getId() == -1) {
return combRepository.findById(id).chain(membreModel -> { return combRepository.findById(id).chain(membreModel -> {

View File

@ -287,14 +287,14 @@ public class MembreService {
if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getEmail() != null && !model.getEmail().isBlank()) {
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
LOGGER.info("Similar membres found: " + model); 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(), if (StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
LOGGER.info("Similar membres found: " + model); 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) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank()) 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()) .chain(membreModel -> clubRepository.findById(membre.getClub())
.map(club -> new Pair<>(membreModel, club))) .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) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank()) if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser"); throw new DBadRequestException("Email déjà utilisé");
}))) })))
.invoke(Unchecked.consumer(membreModel -> { .invoke(Unchecked.consumer(membreModel -> {
if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
@ -492,7 +492,7 @@ public class MembreService {
return clubRepository.findById(input.getClub()) return clubRepository.findById(input.getClub())
.call(__ -> repository.count("email LIKE ?1", input.getEmail()) .call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> { .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 -> { .chain(clubModel -> {
MembreModel model = getMembreModel(input, clubModel); MembreModel model = getMembreModel(input, clubModel);
@ -508,7 +508,7 @@ public class MembreService {
return repository.find("userId = ?1", subject).firstResult() return repository.find("userId = ?1", subject).firstResult()
.call(__ -> repository.count("email LIKE ?1", input.getEmail()) .call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> { .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 -> .call(membreModel ->
repository.count( repository.count(

View File

@ -118,7 +118,7 @@ public class LicenceEndpoints {
@RolesAllowed("federation_admin") @RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(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 = { @APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"), @APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"), @APIResponse(responseCode = "403", description = "Accès refusé"),
@ -128,6 +128,21 @@ public class LicenceEndpoints {
return licenceService.valideLicences(ids); 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<Long> ids) {
return licenceService.validePaymentLicences(ids);
}
@DELETE @DELETE
@Path("{id}") @Path("{id}")
@RolesAllowed("federation_admin") @RolesAllowed("federation_admin")

View File

@ -4,13 +4,14 @@ import {AxiosError} from "../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom"; 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 {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx"; import {SearchBar} from "../components/SearchBar.jsx";
import {ConfirmDialog} from "../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons"; import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
import "./PayAndValidateList.css"; import "./PayAndValidateList.css";
import * as Tools from "../utils/Tools.js";
export function PayAndValidateList({source}) { export function PayAndValidateList({source}) {
@ -22,6 +23,7 @@ export function PayAndValidateList({source}) {
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]); const [licenceData, setLicenceData] = useState([]);
const [clubFilter, setClubFilter] = useState(""); const [clubFilter, setClubFilter] = useState("");
const [catFilter, setCatFilter] = useState("");
const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2) const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2)
const [lastSearch, setLastSearch] = useState(""); const [lastSearch, setLastSearch] = useState("");
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2); const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
@ -34,15 +36,15 @@ export function PayAndValidateList({source}) {
data, data,
error, error,
refresh 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(() => { useEffect(() => {
sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers)); sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers));
}, [selectedMembers]); }, [selectedMembers]);
useEffect(() => { useEffect(() => {
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}`);
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]); }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]);
useEffect(() => { useEffect(() => {
if (!data) if (!data)
@ -62,7 +64,7 @@ export function PayAndValidateList({source}) {
setMemberData(data2); setMemberData(data2);
}, [data, licenceData]); }, [data, licenceData]);
useEffect(() => { const fetchLicenceData = () => {
toast.promise( toast.promise(
apiAxios.get(`/licence/current/${source}`), apiAxios.get(`/licence/current/${source}`),
{ {
@ -77,6 +79,10 @@ export function PayAndValidateList({source}) {
.then(data => { .then(data => {
setLicenceData(data.data); setLicenceData(data.data);
}); });
};
useEffect(() => {
fetchLicenceData();
}, []); }, []);
const search = (search) => { const search = (search) => {
@ -104,7 +110,31 @@ export function PayAndValidateList({source}) {
} }
).then(() => { ).then(() => {
setSelectedMembers([]); 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}) {
<div className="card-body"> <div className="card-body">
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source} <FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter} stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
setPaymentFilter={setPaymentFilter}/> setPaymentFilter={setPaymentFilter} setCatFilter={setCatFilter} catFilter={catFilter}/>
</div> </div>
</div> </div>
<div className="mb-4"> <div className="mb-4">
{source === "admin" && <> {source === "admin" && <>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation">Valider <button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-pay">Valider le payement
des {selectedMembers.length} licences sélectionnée
</button>
<ConfirmDialog title="Payment des licences"
message={"Êtes-vous sûr de vouloir marquer comme payées les " + selectedMembers.length + " licences ?"}
onConfirm={handlePayment} id="confirm-pay"/>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation" style={{marginTop: "0.5em"}}>Valider
les {selectedMembers.length} licences sélectionnée les {selectedMembers.length} licences sélectionnée
</button> </button>
<ConfirmDialog title="Validation des licences" <ConfirmDialog title="Validation des licences"
@ -385,7 +422,7 @@ function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
let allClub = [] let allClub = []
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) { function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter, setCatFilter, catFilter}) {
useEffect(() => { useEffect(() => {
if (!data) if (!data)
return; return;
@ -395,6 +432,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
return <div> return <div>
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>} {source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
<div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option>
{Tools.CatList.map(cat => (
<option key={cat} value={cat}>{getCatName(cat)}</option>
))}
</select>
</div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}> <select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
<option value={1}>Avec demande ou licence validée</option> <option value={1}>Avec demande ou licence validée</option>

View File

@ -67,10 +67,10 @@ export function InformationForm({data}) {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
} }
}).then(_ => { }).then(_ => {
toast.success('Profile mis à jours avec succès 🎉'); toast.success('Profil mis à jours avec succès 🎉');
}).catch(e => { }).catch(e => {
console.log(e.response) console.log(e.response)
toast.error(errFormater(e,'Échec de la mise à jours du profile 😕')); toast.error(errFormater(e,'Échec de la mise à jours du profil 😕'));
}).finally(() => { }).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)