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..7011e37 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,15 +103,47 @@ 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, String categorie) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; + String categorieFilter; + if (categorie == null || categorie.isBlank()) + categorieFilter = " True"; + else + categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal(); + 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 -> { @@ -120,18 +153,18 @@ public class MembreService { if (club == null || club.isBlank()) { query = repository.find( - "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")", - Sort.ascending("fname", "lname"), finalSearch, ids) + "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, + 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)); + "id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, + 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) + "id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, + sort, finalSearch, club, ids) .page(Page.ofSize(limit)); } } @@ -140,7 +173,7 @@ public class MembreService { } public Uni> search(int limit, int page, String search, int licenceRequest, int payState, - String subject) { + String order, String categorie, String subject) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; @@ -149,6 +182,16 @@ public class MembreService { Uni> baseUni = getLicenceListe(licenceRequest, payState); + String categorieFilter; + if (categorie == null || categorie.isBlank()) + categorieFilter = " True"; + else + categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal(); + + 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 -> { @@ -157,8 +200,8 @@ public class MembreService { return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> { PanacheQuery query = repository.find( - "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")", - Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub(), ids) + "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, + 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..543dcd7 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java @@ -58,13 +58,15 @@ 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 = "Catégorie à filter") @QueryParam("categorie") String categorie, + @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, categorie); } @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..6ddca01 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java @@ -50,13 +50,15 @@ public class MembreClubEndpoints { @Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit, @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Text à rechercher") @QueryParam("search") String search, + @Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie, @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, categorie, securityCtx.getSubject()); } @GET diff --git a/src/main/webapp/src/components/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx index 0fd9602..abe62ab 100644 --- a/src/main/webapp/src/components/MemberCustomFiels.jsx +++ b/src/main/webapp/src/components/MemberCustomFiels.jsx @@ -1,5 +1,5 @@ import {useEffect, useState} from "react"; -import {getCategoryFormBirthDate} from "../utils/Tools.js"; +import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js"; import {useCountries} from "../hooks/useCountries.jsx"; export function BirthDayField({inti_date, inti_category, required = true}) { @@ -27,7 +27,7 @@ export function BirthDayField({inti_date, inti_category, required = true}) {
Catégorie {canUpdate && } diff --git a/src/main/webapp/src/components/SearchBar.jsx b/src/main/webapp/src/components/SearchBar.jsx index 6140436..172f668 100644 --- a/src/main/webapp/src/components/SearchBar.jsx +++ b/src/main/webapp/src/components/SearchBar.jsx @@ -7,8 +7,8 @@ const removeDiacritics = str => { } -export function SearchBar({search}) { - const [searchInput, setSearchInput] = useState(""); +export function SearchBar({search, defaultValue = ""}) { + const [searchInput, setSearchInput] = useState(defaultValue); const handelChange = (e) => { setSearchInput(e.target.value); @@ -40,4 +40,4 @@ export function SearchBar({search}) {
-} \ No newline at end of file +} diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx index 112fef8..f454860 100644 --- a/src/main/webapp/src/pages/MemberList.jsx +++ b/src/main/webapp/src/pages/MemberList.jsx @@ -6,38 +6,60 @@ import {useEffect, useState} from "react"; import {useLocation, useNavigate} from "react-router-dom"; import {Checkbox} from "../components/MemberCustomFiels.jsx"; import * as Tools from "../utils/Tools.js"; -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 * as XLSX from "xlsx-js-style"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faEuroSign} from "@fortawesome/free-solid-svg-icons"; + +let lastRefresh = ""; + export function MemberList({source}) { const {hash} = useLocation(); const navigate = useNavigate(); - let page = Number(hash.substring(1)); - page = (page > 0) ? page : 1; const [memberData, setMemberData] = useState([]); const [licenceData, setLicenceData] = useState([]); const [showLicenceState, setShowLicenceState] = useState(false); - const [clubFilter, setClubFilter] = useState(""); - const [stateFilter, setStateFilter] = useState(4) - const [lastSearch, setLastSearch] = useState(""); - const [paymentFilter, setPaymentFilter] = useState(2); + + const setFilter = (filter) => { + navigate("#" + encodeURI(JSON.stringify(filter))) + } + const filter = { + page: 1, + search: "", + club: "", + licenceRequest: 4, + payment: 2, + order: "", + categorie: "", + ...JSON.parse(decodeURI(hash.substring(1)) || "{}"), + } 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=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`, setLoading, 1) useEffect(() => { - refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`); - }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]); + const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`; + if (tmp === lastRefresh) + return; + lastRefresh = tmp + refresh(lastRefresh); + }, [hash]); useEffect(() => { if (!data) return; + if (data.page_count < filter.page) { + setFilter({...filter, page: 1}); + } + const data2 = []; for (const e of data.result) { data2.push({ @@ -74,19 +96,19 @@ export function MemberList({source}) { }, [showLicenceState]); const search = (search) => { - if (search === lastSearch) + if (search === filter.search) return; - setLastSearch(search); + setFilter({...filter, search: search}); } return <>
- + {data ? + page={filter.page} setPage={e => setFilter({...filter, page: e})} source={source}/> : error ? : @@ -102,13 +124,28 @@ export function MemberList({source}) { }
+ +
+
Trie
+
+ setFilter({...filter, order: e.join(",")})} defaultValues={filter.order} source={source}/> +
+
+
Filtre
- + setFilter({...filter, club: e})} + source={source} + stateFilter={filter.licenceRequest} + setStateFilter={e => setFilter({...filter, licenceRequest: e})} + paymentFilter={filter.payment} + setPaymentFilter={e => setFilter({...filter, payment: e})} + catFilter={filter.categorie} + setCatFilter={e => setFilter({...filter, categorie: e})}/>
@@ -336,11 +373,11 @@ function FileInput() { ); } -function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, source}) { +function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) { const pages = [] for (let i = 1; i <= data.page_count; i++) { pages.push(
  • - navigate("#" + i)}>{i} + setPage(i)}>{i}
  • ); } @@ -357,10 +394,10 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
    @@ -369,45 +406,146 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page 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}
    - {source === "club" ? - {member.categorie} - : {member.club?.name || "Sans club"}} +
    + {source === "club" ? + {getCatName(member.categorie)} + :
    {member.club?.name || "Sans club"}
    {getCatName(member.categorie)}
    } +
    + if (showLicenceState && member.licence != null) { - return
    1 ? "warning" : "danger"))} - onClick={() => navigate("" + member.id)}>{rowContent}
    - } else { - return + + } else { + return { + e.preventDefault(); + navigate("" + member.id) + }} + href={"member/" + member.id}> + {rowContent} + } } -let allClub = [] +function OrderBar({onOrderChange, defaultValues = "", source}) { + const [orderCriteria, setOrderCriteria] = useState([...defaultValues.split(",").filter(c => c !== ''), '']); -function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) { - useEffect(() => { - if (!data) - return; - allClub.push(...data.result.map((e) => e.club?.name)) - allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() - }, [data]); + 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 ( + + ); + })} +
    + ); +} + +function FiltreBar({ + showLicenceState, + setShowLicenceState, + clubFilter, + setClubFilter, + source, + stateFilter, + setStateFilter, + paymentFilter, + setPaymentFilter, + catFilter, + setCatFilter, + }) { return
    +
    + +
    {source !== "club" && }