From b479b992cf7f0f1c64ab71b8d1033f6f19750890 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 17 Nov 2025 21:48:24 +0100 Subject: [PATCH] feat: add ordering for member page --- .../ffsaf/domain/service/MembreService.java | 43 ++++++- .../ffsaf/rest/MembreAdminEndpoints.java | 7 +- .../ffsaf/rest/MembreClubEndpoints.java | 5 +- src/main/webapp/src/pages/MemberList.jsx | 111 +++++++++++++++++- 4 files changed, 150 insertions(+), 16 deletions(-) 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 6931e26..bfeccf3 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -13,6 +13,7 @@ import fr.titionfire.ffsaf.rest.data.SimpleMembre; import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData; import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.rest.exception.DInternalError; import fr.titionfire.ffsaf.rest.from.FullMemberForm; import fr.titionfire.ffsaf.utils.*; import io.quarkus.hibernate.reactive.panache.Panache; @@ -102,8 +103,30 @@ public class MembreService { return baseUni; } + private Sort getSort(String order) { + + Sort sort; + if (order == null || order.isBlank()) { + sort = Sort.ascending("fname", "lname"); + } else { + sort = Sort.empty(); + + for (String e : order.split(",")) { + String[] split = e.split(" "); + if (split.length == 2) { + sort = sort.and(split[0], + split[1].equals("n") ? Sort.Direction.Ascending : Sort.Direction.Descending); + } else { + return null; + } + } + } + + return sort; + } + public Uni> searchAdmin(int limit, int page, String search, String club, - int licenceRequest, int payState) { + int licenceRequest, int payState, String order) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; @@ -111,6 +134,10 @@ public class MembreService { String finalSearch = search; Uni> baseUni = getLicenceListe(licenceRequest, payState); + Sort sort = getSort(order); + if (sort == null) + return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie")); + return baseUni .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .chain(ids -> { @@ -121,17 +148,17 @@ public class MembreService { if (club == null || club.isBlank()) { query = repository.find( "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")", - Sort.ascending("fname", "lname"), finalSearch, ids) + sort, finalSearch, ids) .page(Page.ofSize(limit)); } else { if (club.equals("null")) { query = repository.find( "id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ")", - Sort.ascending("fname", "lname"), finalSearch, ids).page(Page.ofSize(limit)); + sort, finalSearch, ids).page(Page.ofSize(limit)); } else { query = repository.find( "id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")", - Sort.ascending("fname", "lname"), finalSearch, club + "%", ids) + sort, finalSearch, club + "%", ids) .page(Page.ofSize(limit)); } } @@ -140,7 +167,7 @@ public class MembreService { } public Uni> search(int limit, int page, String search, int licenceRequest, int payState, - String subject) { + String order, String subject) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; @@ -149,6 +176,10 @@ public class MembreService { Uni> baseUni = getLicenceListe(licenceRequest, payState); + Sort sort = getSort(order); + if (sort == null) + return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie")); + return baseUni .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .chain(ids -> { @@ -158,7 +189,7 @@ public class MembreService { .chain(membreModel -> { PanacheQuery query = repository.find( "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")", - Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub(), ids) + sort, finalSearch, membreModel.getClub(), ids) .page(Page.ofSize(limit)); return getPageResult(query, limit, page); }); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java index e0b04b0..426ef6e 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java @@ -58,13 +58,14 @@ public class MembreAdminEndpoints { @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Text à rechercher") @QueryParam("search") String search, @Parameter(description = "Club à filter") @QueryParam("club") String club, - @Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, - @Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) { + @Parameter(description = "État de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, + @Parameter(description = "État du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment, + @Parameter(description = "Ordre") @QueryParam("order") String order) { if (limit == null) limit = 50; if (page == null || page < 1) page = 1; - return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment); + return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order); } @GET diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java index 545552c..d227cda 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java @@ -51,12 +51,13 @@ public class MembreClubEndpoints { @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Text à rechercher") @QueryParam("search") String search, @Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, - @Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) { + @Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment, + @Parameter(description = "Ordre") @QueryParam("order") String order) { if (limit == null) limit = 50; if (page == null || page < 1) page = 1; - return membreService.search(limit, page - 1, search, licenceRequest, payment, securityCtx.getSubject()); + return membreService.search(limit, page - 1, search, licenceRequest, payment, order, securityCtx.getSubject()); } @GET diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx index 112fef8..8b97c97 100644 --- a/src/main/webapp/src/pages/MemberList.jsx +++ b/src/main/webapp/src/pages/MemberList.jsx @@ -13,6 +13,7 @@ import * as XLSX from "xlsx-js-style"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faEuroSign} from "@fortawesome/free-solid-svg-icons"; + export function MemberList({source}) { const {hash} = useLocation(); const navigate = useNavigate(); @@ -26,14 +27,19 @@ export function MemberList({source}) { const [stateFilter, setStateFilter] = useState(4) const [lastSearch, setLastSearch] = useState(""); const [paymentFilter, setPaymentFilter] = useState(2); + const [order, setOrder] = useState(""); const setLoading = useLoadingSwitcher() - const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1) + const { + data, + error, + refresh + } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&order=${order}`, setLoading, 1) 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}&order=${order}`); + }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, order]); useEffect(() => { if (!data) @@ -79,6 +85,10 @@ export function MemberList({source}) { setLastSearch(search); } + const onOrderChange = (newOrder) => { + setOrder(newOrder.join(",")); + } + return <>
@@ -102,6 +112,14 @@ export function MemberList({source}) { }
+ +
+
Trie
+
+ +
+
+
Filtre
@@ -371,7 +389,7 @@ function MakeRow({member, showLicenceState, navigate, source}) { const rowContent = <>
{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "} - {(showLicenceState && member.licence != null && member.licence.pay)? : <>  } + {(showLicenceState && member.licence != null && member.licence.pay) ? : <>  }
{member.fname} {member.lname}
@@ -394,9 +412,92 @@ function MakeRow({member, showLicenceState, navigate, source}) { } } +function OrderBar({onOrderChange, source}) { + const [orderCriteria, setOrderCriteria] = useState(['']); + + const handleChange = (index, value) => { + const newCriteria = [...orderCriteria]; + newCriteria[index] = value; + + // Si le dernier critère est rempli, on en ajoute un nouveau + if (index === orderCriteria.length - 1 && value !== '') { + newCriteria.push(''); + } + // Si un critère (sauf le premier) est réinitialisé, on le supprime + else if (value === '' && (index !== 0 || orderCriteria.length > 1)) { + newCriteria.splice(index, 1); + } + + setOrderCriteria(newCriteria); + onOrderChange(newCriteria.filter(c => c !== '')); + }; + + // Liste de toutes les options possibles + const allOptions = [ + {value: 'lname n', label: 'Nom ↓', base: 'lname'}, + {value: 'lname i', label: 'Nom ↑', base: 'lname'}, + {value: 'fname n', label: 'Prénom ↓', base: 'fname'}, + {value: 'fname i', label: 'Prénom ↑', base: 'fname'}, + {value: 'categorie n', label: 'Catégorie ↓', base: 'categorie'}, + {value: 'categorie i', label: 'Catégorie ↑', base: 'categorie'}, + {value: 'licence n', label: 'Licence ↓', base: 'licence'}, + {value: 'licence i', label: 'Licence ↑', base: 'licence'}, + ]; + + if (source === "admin") { + allOptions.push( + {value: 'club.name n', label: 'Club ↓', base: 'club.name'}, + {value: 'club.name i', label: 'Club ↑', base: 'club.name'}, + ); + } + + return ( +
+ {orderCriteria.map((criteria, index) => { + // Récupère les bases des critères déjà sélectionnés (sauf le courant) + const usedBases = orderCriteria + .filter((c, i) => c !== '' && i !== index) + .map(c => allOptions.find(o => o.value === c)?.base); + + // Filtre les options disponibles + const availableOptions = allOptions.filter(option => + !usedBases.includes(option.base) || option.value === criteria + ); + + return ( + + ); + })} +
+ ); +} + let allClub = [] -function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) { +function FiltreBar({ + showLicenceState, + setShowLicenceState, + data, + clubFilter, + setClubFilter, + source, + stateFilter, + setStateFilter, + paymentFilter, + setPaymentFilter + }) { useEffect(() => { if (!data) return;