From 0a91e72c2962c0e106f7c344d43c21ced9c7908e Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sun, 4 Jan 2026 21:05:20 +0100 Subject: [PATCH 1/5] feat: autorise null mail for admin add --- .../java/fr/titionfire/ffsaf/domain/service/MembreService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 fa9870b..2f4278e 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -479,7 +479,8 @@ 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à utilisé"); + if (c > 0 && input.getEmail() != null && !input.getEmail().isBlank()) + throw new DBadRequestException("Email déjà utilisé"); }))) .chain(clubModel -> { MembreModel model = getMembreModel(input, clubModel); -- 2.49.0 From 78959fc485df001cb77b52c0ec88576af3ff7ecc Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sun, 4 Jan 2026 21:29:42 +0100 Subject: [PATCH 2/5] feat: cma select comb sort by name --- .../pages/competition/editor/CategoryAdminContent.jsx | 8 +++++++- .../competition/editor/SelectCombModalContent.jsx | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index faae064..e54ad8e 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -38,7 +38,13 @@ export function CategoryContent({cat, catId, setCat, menuActions}) { }, [groups]); function readAndConvertMatch(matches, data, combsToAdd) { - matches.push({...data, c1: data.c1?.id, c2: data.c2?.id}) + matches.push({ + ...data, + c1: data.c1?.id, + c2: data.c2?.id, + c1_cacheName: data.c1?.fname + " " + data.c1?.lname, + c2_cacheName: data.c2?.fname + " " + data.c2?.lname + }) if (data.c1) combsToAdd.push(data.c1) if (data.c2) diff --git a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx index 2977c26..0240fe3 100644 --- a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx @@ -243,7 +243,7 @@ export function SelectCombModalContent({data, setGroups}) {
Inscrit
{dispoFiltered && Object.keys(dispoFiltered).length === 0 &&
Aucun combattant disponible
} - {Object.keys(dispoFiltered).map((id) => ( + {Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
} + +function nameCompare(data, a, b) { + const combA = data.find(d => d.id === Number(a)); + const combB = data.find(d => d.id === Number(b)); + return (combA.fname + " " + combA.lname).toLowerCase().localeCompare((combB.fname + " " + combB.lname).toLowerCase()); +} -- 2.49.0 From a871b520062c111ee55c2b1678d804957df7ca5e Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 5 Jan 2026 11:09:22 +0100 Subject: [PATCH 3/5] feat: use blason setting --- .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 4 +++ .../titionfire/ffsaf/ws/data/WelcomeInfo.java | 2 ++ src/main/webapp/src/hooks/useComb.jsx | 4 +-- src/main/webapp/src/hooks/useWS.jsx | 31 ++++++++++++++----- .../src/pages/competition/editor/CMTable.jsx | 12 +++---- .../editor/CompetitionManagerRoot.jsx | 14 +++------ .../pages/competition/editor/PubAffWindow.jsx | 9 ++++-- 7 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index c22e5ef..d5246f3 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import fr.titionfire.ffsaf.data.repository.CompetitionRepository; import fr.titionfire.ffsaf.domain.service.CompetPermService; import fr.titionfire.ffsaf.net2.MessageType; +import fr.titionfire.ffsaf.rest.data.SimpleCompetData; import fr.titionfire.ffsaf.utils.SecurityCtx; import fr.titionfire.ffsaf.ws.data.WelcomeInfo; import fr.titionfire.ffsaf.ws.recv.*; @@ -118,10 +119,13 @@ public class CompetitionWS { waitingResponse.put(connection, new HashMap<>()); }) .map(cm -> { + SimpleCompetData data = SimpleCompetData.fromModel(cm); WelcomeInfo welcomeInfo = new WelcomeInfo(); welcomeInfo.setName(cm.getName()); welcomeInfo.setPerm(connection.userData().get(UserData.TypedKey.forString("prem"))); + welcomeInfo.setShow_blason(data.isShow_blason()); + welcomeInfo.setShow_flag(data.isShow_flag()); return new MessageOut(UUID.randomUUID(), "welcomeInfo", MessageType.NOTIFY, welcomeInfo); }); diff --git a/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java b/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java index eebe679..a1dff22 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/data/WelcomeInfo.java @@ -8,4 +8,6 @@ import lombok.Data; public class WelcomeInfo { private String name; private String perm; + private boolean show_blason; + private boolean show_flag; } diff --git a/src/main/webapp/src/hooks/useComb.jsx b/src/main/webapp/src/hooks/useComb.jsx index 4f1fb7b..aa62ac0 100644 --- a/src/main/webapp/src/hooks/useComb.jsx +++ b/src/main/webapp/src/hooks/useComb.jsx @@ -25,7 +25,7 @@ function reducer(state, action) { country: action.payload.data.country, }) if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) { - console.debug("Updating comb", comb); + //console.debug("Updating comb", comb); return { ...state, [comb.id]: comb @@ -49,7 +49,7 @@ function reducer(state, action) { for (const o of combs) { newCombs[o.id] = o; } - console.debug("Updating combs", newCombs); + //console.debug("Updating combs", newCombs); return { ...state, diff --git a/src/main/webapp/src/hooks/useWS.jsx b/src/main/webapp/src/hooks/useWS.jsx index ef5f0c0..79fce96 100644 --- a/src/main/webapp/src/hooks/useWS.jsx +++ b/src/main/webapp/src/hooks/useWS.jsx @@ -44,6 +44,7 @@ export function WSProvider({url, onmessage, children}) { const {is_authenticated} = useAuth() const [isReady, setIsReady] = useState(false) const [doReconnect, setDoReconnect] = useState(false) + const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false}) const [state, dispatch] = useReducer(reducer, {listener: []}) const ws = useRef(null) const listenersRef = useRef([]) @@ -58,6 +59,15 @@ export function WSProvider({url, onmessage, children}) { listenersRef.current = state.listener }, [state.listener]) + const welcomeListener = ({data}) => { + setWelcomeData({ + name: data.name, + perm: data.perm, + show_blason: data.show_blason, + show_flag: data.show_flag + }) + } + useEffect(() => { if (!doReconnect && !is_authenticated && isReady) return; @@ -73,7 +83,7 @@ export function WSProvider({url, onmessage, children}) { newSocket.onclose = ws.current.onclose newSocket.onmessage = ws.current.onmessage ws.current = newSocket - }catch (e) { + } catch (e) { } }, 5000); @@ -87,7 +97,7 @@ export function WSProvider({url, onmessage, children}) { setDoReconnect(true) console.log(`WSProvider ${id} mounted ${mountCounter[id]} time(s)`); - if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)){ + if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)) { console.log("WSProvider: connecting to", url); const socket = new WebSocket(url) @@ -122,6 +132,12 @@ export function WSProvider({url, onmessage, children}) { console.error("Listener callback error:", err) } }); + + if (msg.code === 'welcomeInfo') { + welcomeListener({...msg}) + isHandled = true; + } + if (!isHandled && onmessage) onmessage(JSON.parse(event.data)) } @@ -170,7 +186,7 @@ export function WSProvider({url, onmessage, children}) { } } - console.log("WSProvider: sending message", {uuid, code, type, data}); + //console.log("WSProvider: sending message", {uuid, code, type, data}); ws.current?.send(JSON.stringify({ uuid: uuid, code: code, @@ -183,7 +199,7 @@ export function WSProvider({url, onmessage, children}) { }) => { if (isReadyRef.current) { send2(uuid, code, type, data, resolve, reject); - }else { + } else { let counter = 0; const waitInterval = setInterval(() => { if (isReadyRef.current) { @@ -200,18 +216,19 @@ export function WSProvider({url, onmessage, children}) { } - const ret = {isReady, dispatch, send, wait_length: callbackRef} + const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData} return {children} } export function useWS() { - const {isReady, dispatch, send, wait_length} = useContext(WebsocketContext) + const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext) return { dispatch, isReady, wait_length, + welcomeData, sendRequest: (code, data) => { return new Promise((resolve, reject) => { send(uuidv4(), code, "REQUEST", data, resolve, reject); @@ -255,7 +272,7 @@ export function useRequestWS(code, payload, setLoading = null, loadingLevel = 1) useEffect(() => { if (isReady) refresh(code, payload) - else{ + else { if (setLoading) setLoading(loadingLevel) setTimeout(() => refresh(code, payload), 1000) diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 287b77e..f0d6980 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef, useState} from "react"; -import {useRequestWS} from "../../../hooks/useWS.jsx"; +import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {createPortal} from "react-dom"; @@ -13,7 +13,6 @@ import {CategorieSelect} from "./CMTMatchPanel.jsx"; import {PointPanel} from "./CMTPoint.jsx"; import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; -import {timePrint} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; export function CMTable() { @@ -235,6 +234,7 @@ function ObsAutoSyncWhitPubAff() { const {connected, setText, setTextAndColor, setDiapo} = useOBS(); const oldState = useRef({timeColor: "#000000", timeStr: "--:--", c1: null, c2: null, showScore: true, scoreRouge: 0, scoreBleu: 0}); const state = usePubAffState(); + const {welcomeData} = useWS(); const {getComb} = useCombs(); useEffect(() => { @@ -242,8 +242,8 @@ function ObsAutoSyncWhitPubAff() { const comb = getComb(state.c1); setText("comb.rouge", comb ? (comb?.fname + " " + comb?.lname) : ""); const files = [] - if (comb?.club_uuid) files.push(`club_${comb.club_uuid}.png`) - if (comb?.country) files.push(`flag_${comb.country.toLowerCase()}.png`) + if (comb?.club_uuid && welcomeData.show_blason) files.push(`club_${comb.club_uuid}.png`) + if (comb?.country && welcomeData.show_flag) files.push(`flag_${comb.country.toLowerCase()}.png`) setDiapo("img.rouge", files); oldState.current.c1 = state.c1; } @@ -252,8 +252,8 @@ function ObsAutoSyncWhitPubAff() { const comb = getComb(state.c2); setText("comb.blue", comb ? (comb?.fname + " " + comb?.lname) : ""); const files = [] - if (comb?.club_uuid) files.push(`club_${comb.club_uuid}.png`) - if (comb?.country) files.push(`flag_${comb.country.toLowerCase()}.png`) + if (comb?.club_uuid && welcomeData.show_blason) files.push(`club_${comb.club_uuid}.png`) + if (comb?.country && welcomeData.show_flag) files.push(`flag_${comb.country.toLowerCase()}.png`) setDiapo("img.blue", files); oldState.current.c2 = state.c2; } diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx index 59e7c3c..2a535e5 100644 --- a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx +++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx @@ -75,9 +75,8 @@ function HomeComp() { } function WSStatus({setPerm}) { - const [name, setName] = useState("") const [inWait, setInWait] = useState(false) - const {isReady, wait_length, dispatch} = useWS(); + const {isReady, wait_length, welcomeData} = useWS(); useEffect(() => { const timer = setInterval(() => { @@ -87,16 +86,11 @@ function WSStatus({setPerm}) { }, []); useEffect(() => { - const welcomeListener = ({data}) => { - setName(data.name) - setPerm(data.perm) - } - dispatch({type: 'addListener', payload: {callback: welcomeListener, code: 'welcomeInfo'}}) - return () => dispatch({type: 'removeListener', payload: welcomeListener}) - }, []) + setPerm(welcomeData.perm) + }, [welcomeData]) return
-

{name}

+

{welcomeData.name}

Serveur:
diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx index 0b7273d..3b8cd31 100644 --- a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -2,6 +2,7 @@ import {useCombs} from "../../../hooks/useComb.jsx"; import {usePubAffState} from "../../../hooks/useExternalWindow.jsx"; import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx"; import {useMemo, useRef} from 'react'; +import {useWS} from "../../../hooks/useWS.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -124,6 +125,7 @@ const logoStyle = {width: "6vw", height: "min(11vh, 6vw)", objectFit: "contain", function CombDisplay({combId, background, children}) { const {getComb} = useCombs(); const comb = getComb(combId, ""); + const {welcomeData} = useWS(); const logoAlt = useMemo(() => { return comb?.club_str @@ -142,10 +144,11 @@ function CombDisplay({combId, background, children}) { alignItems: "center", }}> {comb !== "" && <> - + {welcomeData.show_blason && }
{comb.fname} {comb.lname}
- {comb.country} + {welcomeData.show_flag ? {comb.country} + :
} }
{children} -- 2.49.0 From 0757ae71989aadf62325d08b24a7089fadd754ce Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 5 Jan 2026 14:20:22 +0100 Subject: [PATCH 4/5] feat: allow admin and club admin to get comp result --- .../ffsaf/domain/service/ResultService.java | 89 ++++++++++++++----- .../ffsaf/rest/ExternalResultEndpoints.java | 5 +- .../ffsaf/rest/ResultEndpoints.java | 9 +- 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index 934322d..ca07ad5 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -93,12 +93,21 @@ public class ResultService { public Uni> getList(SecurityCtx securityCtx) { return membreService.getByAccountId(securityCtx.getSubject()) - .chain(m -> registerRepository.list("membre = ?1", m)) + .chain(m -> registerRepository.list( + "membre = ?1 OR (TRUE = ?2 AND membre.club = ?3)", + m, securityCtx.isClubAdmin(), m.getClub())) .onItem().transformToMulti(Multi.createFrom()::iterable) .onItem().call(r -> Mutiny.fetch(r.getCompetition())) - .onItem().transform(r -> new Object[]{r.getCompetition().getUuid(), r.getCompetition().getName(), - r.getCompetition().getDate()}) - .collect().asList(); + .onItem().transform(RegisterModel::getCompetition) + .collect().asList() + .chain(l -> compRepository.list("owner = ?1 OR ?1 IN admin", securityCtx.getSubject()) + .map(l2 -> Stream.concat(l.stream(), l2.stream()).distinct() + .map(c -> new Object[]{c.getUuid(), c.getName(), c.getDate()}).toList()) + ); + } + + public Uni> getCategoryList(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx).chain(__ -> getCategoryList(uuid)); } public Uni> getCategoryList(String uuid) { @@ -113,11 +122,11 @@ public class ResultService { } public Uni getCategory(String uuid, long poule, SecurityCtx securityCtx) { - return hasAccess(uuid, securityCtx).chain(r -> + return hasAccess(uuid, securityCtx).chain(membreModel -> matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule) .call(list -> list.isEmpty() ? Uni.createFrom().voidItem() : Mutiny.fetch(list.get(0).getCategory().getTree())) - .map(list -> getData(list, r.getMembre()))); + .map(list -> getData(list, membreModel))); } public Uni getCategory(String uuid, long poule) { @@ -242,7 +251,7 @@ public class ResultService { public Uni getAllCombArray(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) - .chain(r -> getAllCombArray_(uuid, r.getMembre())); + .chain(membreModel -> getAllCombArray_(uuid, membreModel)); } public Uni getAllCombArrayPublic(String uuid) { @@ -315,7 +324,16 @@ public class ResultService { }); } - public Uni> getCombList(String uuid, ResultPrivacy privacy) { + public Uni> getCombList(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx) + .chain(membreModel -> getCombList(uuid, ResultPrivacy.REGISTERED_ONLY)); + } + + public Uni> getCombList(String uuid) { + return getCombList(uuid, ResultPrivacy.PUBLIC); + } + + private Uni> getCombList(String uuid, ResultPrivacy privacy) { return registerRepository.list("competition.uuid = ?1 AND membre.resultPrivacy <= ?2", uuid, privacy) .map(models -> { HashMap map = new HashMap<>(); @@ -332,7 +350,16 @@ public class ResultService { ); } - public Uni getCombArrayPublic(String uuid, String combTempId, ResultPrivacy privacy) { + public Uni getCombArrayPublic(String uuid, String combTempId, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx) + .chain(membreModel -> getCombArrayPublic(uuid, combTempId, ResultPrivacy.REGISTERED_ONLY)); + } + + public Uni getCombArrayPublic(String uuid, String combTempId) { + return getCombArrayPublic(uuid, combTempId, ResultPrivacy.PUBLIC); + } + + private Uni getCombArrayPublic(String uuid, String combTempId, ResultPrivacy privacy) { CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder(); Long id = getCombTempId(combTempId); @@ -471,6 +498,10 @@ public class ResultService { } } + public Uni> getClubList(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx).chain(__ -> getClubList(uuid)); + } + public Uni> getClubList(String uuid) { return registerRepository.list("competition.uuid = ?1", uuid) .map(registers -> { @@ -491,7 +522,7 @@ public class ResultService { } public Uni getClubArray(String uuid, Long id, SecurityCtx securityCtx) { - return hasAccess(uuid, securityCtx).chain(cm_register -> getClubArray2(uuid, id, cm_register.getMembre())); + return hasAccess(uuid, securityCtx).chain(membreModel -> getClubArray2(uuid, id, membreModel)); } public Uni getClubArray2(String uuid, Long id, MembreModel membreModel) { @@ -620,21 +651,35 @@ public class ResultService { } } - private Uni hasAccess(String uuid, SecurityCtx securityCtx) { + private Uni hasAccess(String uuid, SecurityCtx securityCtx) { return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid) .firstResult() - .invoke(Unchecked.consumer(o -> { - if (o == null) - throw new DForbiddenException("Access denied"); - })); - } + .chain(Unchecked.function(o -> { + if (o != null) + return Uni.createFrom().item(o.getMembre()); - private Uni hasAccess(Long compId, SecurityCtx securityCtx) { - return registerRepository.find("membre.userId = ?1 AND competition.id = ?2", securityCtx.getSubject(), compId) - .firstResult() - .invoke(Unchecked.consumer(o -> { - if (o == null) - throw new DForbiddenException("Access denied"); + return membreService.getByAccountId(securityCtx.getSubject()).chain(m -> { + if (securityCtx.isClubAdmin()) { + return registerRepository.count("membre.club = ?2 AND competition.uuid = ?1", + uuid, m.getClub()).chain(c -> { + if (c > 0) return Uni.createFrom().item(m); + + return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)", + uuid, securityCtx.getSubject()) + .chain(c2 -> { + if (c2 > 0) return Uni.createFrom().item(m); + return Uni.createFrom().failure(new DForbiddenException("Access denied")); + }); + }); + } else { + return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)", uuid, + securityCtx.getSubject()) + .chain(c2 -> { + if (c2 > 0) return Uni.createFrom().item(m); + return Uni.createFrom().failure(new DForbiddenException("Access denied")); + }); + } + }); })); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java index 2febbd1..259c3bb 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java @@ -2,7 +2,6 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.ResultService; import fr.titionfire.ffsaf.domain.service.UpdateService; -import fr.titionfire.ffsaf.utils.ResultPrivacy; import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.ws.rs.*; @@ -47,7 +46,7 @@ public class ExternalResultEndpoints { @Path("/comb/list") @Produces(MediaType.APPLICATION_JSON) public Uni> combList() { - return resultService.getCombList(id, ResultPrivacy.PUBLIC); + return resultService.getCombList(id); } @GET @@ -56,7 +55,7 @@ public class ExternalResultEndpoints { public Uni getArray(@QueryParam("comb") String comb) { if (comb.equals("0")) return Uni.createFrom().item(""); - return resultService.getCombArrayPublic(id, comb, ResultPrivacy.PUBLIC); + return resultService.getCombArrayPublic(id, comb); } @GET diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java index e1449fb..25e1a6e 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java @@ -2,7 +2,6 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.ResultService; import fr.titionfire.ffsaf.rest.data.ResultCategoryData; -import fr.titionfire.ffsaf.utils.ResultPrivacy; import fr.titionfire.ffsaf.utils.SecurityCtx; import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; @@ -33,7 +32,7 @@ public class ResultEndpoints { @GET @Path("{uuid}/category/list") public Uni> getCategoryList(@PathParam("uuid") String uuid) { - return resultService.getCategoryList(uuid); + return resultService.getCategoryList(uuid, securityCtx); } @GET @@ -45,7 +44,7 @@ public class ResultEndpoints { @GET @Path("{uuid}/club/list") public Uni> getClubList(@PathParam("uuid") String uuid) { - return resultService.getClubList(uuid); + return resultService.getClubList(uuid, securityCtx); } @GET @@ -57,13 +56,13 @@ public class ResultEndpoints { @GET @Path("{uuid}/comb/list") public Uni> getCombList(@PathParam("uuid") String uuid) { - return resultService.getCombList(uuid, ResultPrivacy.REGISTERED_ONLY); + return resultService.getCombList(uuid, securityCtx); } @GET @Path("{uuid}/comb/{id}") public Uni getCombList(@PathParam("uuid") String uuid, @PathParam("id") String id) { - return resultService.getCombArrayPublic(uuid, id, ResultPrivacy.REGISTERED_ONLY); + return resultService.getCombArrayPublic(uuid, id, securityCtx); } @GET -- 2.49.0 From 13650b27c1b54cb32a225f3c30d46fa754287d3c Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 5 Jan 2026 17:56:37 +0100 Subject: [PATCH 5/5] feat: add selection --- .../titionfire/ffsaf/data/model/LogModel.java | 2 +- .../ffsaf/data/model/MembreModel.java | 4 + .../ffsaf/data/model/SelectionModel.java | 44 ++++ .../data/repository/SelectionRepository.java | 9 + .../ffsaf/domain/service/MembreService.java | 14 +- .../domain/service/SelectionService.java | 64 ++++++ .../ffsaf/rest/LicenceEndpoints.java | 2 +- .../ffsaf/rest/SelectionEndpoints.java | 85 +++++++ .../fr/titionfire/ffsaf/rest/data/MeData.java | 2 + .../ffsaf/rest/data/SimpleSelection.java | 36 +++ src/main/webapp/src/pages/MePage.jsx | 13 +- .../webapp/src/pages/admin/club/ClubPage.jsx | 2 +- .../src/pages/admin/club/NewClubPage.jsx | 2 +- .../pages/admin/member/InformationForm.jsx | 2 +- .../src/pages/admin/member/MemberPage.jsx | 14 +- .../src/pages/admin/member/SelectCard.jsx | 208 ++++++++++++++++++ .../webapp/src/pages/club/club/MyClubPage.jsx | 2 +- .../src/pages/club/member/InformationForm.jsx | 2 +- .../src/pages/club/member/MemberPage.jsx | 14 +- .../src/pages/club/member/SelectCard.jsx | 27 +++ 20 files changed, 507 insertions(+), 41 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/SelectionModel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/SelectionRepository.java create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/SelectionService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/SelectionEndpoints.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/SimpleSelection.java create mode 100644 src/main/webapp/src/pages/admin/member/SelectCard.jsx create mode 100644 src/main/webapp/src/pages/club/member/SelectCard.jsx diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java index 4d1dd9d..dbb770f 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java @@ -41,7 +41,7 @@ public class LogModel { } public enum ObjectType { - Membre, Affiliation, Licence, Club, Competition, Register + Membre, Affiliation, Licence, Club, Competition, Register, Selection } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java index d83f232..0e8a420 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java @@ -72,6 +72,10 @@ public class MembreModel implements LoggableModel, CombModel { @Schema(description = "Les licences du membre. (optionnel)") List licences; + @OneToMany(mappedBy = "membre", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Schema(description = "Les séléctions du membre. (optionnel)") + List selections; + @Override public String getObjectName() { return fname + " " + lname; diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/SelectionModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/SelectionModel.java new file mode 100644 index 0000000..e4b010b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/SelectionModel.java @@ -0,0 +1,44 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.Categorie; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "selection") +public class SelectionModel implements LoggableModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "L'identifiant de la séléction.") + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre", referencedColumnName = "id") + @Schema(description = "Le membre de la séléction. (optionnel)") + MembreModel membre; + + @Schema(description = "La saison de la séléction.", examples = "2025") + int saison; + + @Schema(description = "Catégorie de la séléction.") + Categorie categorie; + + @Override + public String getObjectName() { + return "selection " + id.toString(); + } + + @Override + public LogModel.ObjectType getObjectType() { + return LogModel.ObjectType.Selection; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/SelectionRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/SelectionRepository.java new file mode 100644 index 0000000..0aebfa1 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/SelectionRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.SelectionModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class SelectionRepository implements PanacheRepositoryBase { +} 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 2f4278e..39fc2b2 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -7,10 +7,7 @@ import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleCombModel; import fr.titionfire.ffsaf.net2.request.SReqComb; -import fr.titionfire.ffsaf.rest.data.MeData; -import fr.titionfire.ffsaf.rest.data.SimpleLicence; -import fr.titionfire.ffsaf.rest.data.SimpleMembre; -import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData; +import fr.titionfire.ffsaf.rest.data.*; import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DInternalError; @@ -590,9 +587,12 @@ public class MembreService { MeData meData = new MeData(); return repository.find("userId = ?1", subject).firstResult() .invoke(meData::setMembre) - .chain(membreModel -> Mutiny.fetch(membreModel.getLicences())) - .map(licences -> licences.stream().map(SimpleLicence::fromModel).toList()) - .invoke(meData::setLicences) + .call(membreModel -> Mutiny.fetch(membreModel.getLicences()) + .map(licences -> licences.stream().map(SimpleLicence::fromModel).toList()) + .invoke(meData::setLicences)) + .call(membreModel -> Mutiny.fetch(membreModel.getSelections()) + .map(licences -> licences.stream().map(SimpleSelection::fromModel).toList()) + .invoke(meData::setSelections)) .map(__ -> meData); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/SelectionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/SelectionService.java new file mode 100644 index 0000000..7ec979a --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/SelectionService.java @@ -0,0 +1,64 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.LogModel; +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.data.model.SelectionModel; +import fr.titionfire.ffsaf.data.repository.CombRepository; +import fr.titionfire.ffsaf.data.repository.SelectionRepository; +import fr.titionfire.ffsaf.rest.data.SimpleSelection; +import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.List; +import java.util.function.Consumer; + +@WithSession +@ApplicationScoped +public class SelectionService { + + @Inject + CombRepository combRepository; + + @Inject + SelectionRepository repository; + + @Inject + LoggerService ls; + + public Uni> getSelection(long id, Consumer checkPerm) { + return combRepository.findById(id).invoke(checkPerm) + .chain(combRepository -> Mutiny.fetch(combRepository.getSelections())); + } + + public Uni setSelection(long id, SimpleSelection data) { + if (data.getId() == -1) { + return combRepository.findById(id).chain(membreModel -> { + SelectionModel model = new SelectionModel(); + + model.setMembre(membreModel); + model.setSaison(data.getSaison()); + model.setCategorie(data.getCategorie()); + return Panache.withTransaction(() -> repository.persist(model)) + .call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(), + licenceModel)); + }); + } else { + return repository.findById(data.getId()).chain(model -> { + ls.logChange("Catégorie", model.getCategorie(), data.getCategorie(), model); + model.setCategorie(data.getCategorie()); + return Panache.withTransaction(() -> repository.persist(model)) + .call(__ -> ls.append()); + }); + } + } + + public Uni deleteSelection(long id) { + return repository.findById(id) + .call(model -> ls.logADelete(model)) + .chain(model -> Panache.withTransaction(() -> repository.delete(model))); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java index efcd9d8..cd8b8e1 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java @@ -101,7 +101,7 @@ public class LicenceEndpoints { @RolesAllowed("federation_admin") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.MULTIPART_FORM_DATA) - @Operation(summary = "Créer une licence", description = "Créer unr licence en fonction de son identifiant et des " + + @Operation(summary = "Créer une licence", description = "Créer une licence en fonction de son identifiant et des " + "informations fournies dans le formulaire (pour les administrateurs)") @APIResponses(value = { @APIResponse(responseCode = "200", description = "La licence a été mise à jour avec succès"), diff --git a/src/main/java/fr/titionfire/ffsaf/rest/SelectionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/SelectionEndpoints.java new file mode 100644 index 0000000..4e3c31e --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/SelectionEndpoints.java @@ -0,0 +1,85 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.domain.service.SelectionService; +import fr.titionfire.ffsaf.rest.data.SimpleSelection; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +import java.util.List; +import java.util.function.Consumer; + +@Path("api/selection") +public class SelectionEndpoints { + + @Inject + SelectionService selectionService; + + @Inject + SecurityCtx securityCtx; + + Consumer checkPerm = Unchecked.consumer(membreModel -> { + if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(membreModel.getClub().getId())) + throw new DForbiddenException(); + }); + + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra", + "ffsaf_selectionneur"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Renvoie les séléctions d'un membre", description = "Renvoie les séléctions d'un membre en fonction " + + "de son identifiant") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La liste des séléctions du membre"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "Le membre n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getSelection(@PathParam("id") long id) { + return selectionService.getSelection(id, checkPerm) + .map(selectionModels -> selectionModels.stream().map(SimpleSelection::fromModel).toList()); + } + + @POST + @Path("{id}") + @RolesAllowed({"federation_admin", "ffsaf_selectionneur"}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Créer une séléction", description = "Créer une séléction en fonction de son identifiant et des " + + "informations fournies dans le formulaire (pour les administrateurs)") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "La séléction a été mise à jour avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "La séléction n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni setSelection(@PathParam("id") long id, SimpleSelection data) { + return selectionService.setSelection(id, data).map(SimpleSelection::fromModel); + } + + @DELETE + @Path("{id}") + @RolesAllowed({"federation_admin", "ffsaf_selectionneur"}) + @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Supprime une séléction", description = "Supprime une séléction en fonction de son identifiant " + + "(pour les administrateurs)") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "La séléction a été supprimée avec succès"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "La séléction n'existe pas"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni deleteSelection(@PathParam("id") long id) { + return selectionService.deleteSelection(id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java index ece32cc..7da843e 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java @@ -44,6 +44,8 @@ public class MeData { private ResultPrivacy resultPrivacy; @Schema(description = "La liste des licences du membre.") private List licences; + @Schema(description = "La liste des séléctions du membre.") + private List selections; public void setMembre(MembreModel membreModel) { this.id = membreModel.getId(); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleSelection.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleSelection.java new file mode 100644 index 0000000..4c37b7a --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleSelection.java @@ -0,0 +1,36 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.SelectionModel; +import fr.titionfire.ffsaf.utils.Categorie; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Data +@Builder +@AllArgsConstructor +@RegisterForReflection +public class SimpleSelection { + @Schema(description = "ID de la séléction", examples = "1") + Long id; + @Schema(description = "ID du membre", examples = "1") + Long membre; + @Schema(description = "Saison de la séléction", examples = "2024") + int saison; + @Schema(description = "Catégorie de la séléction", examples = "JUNIOR") + Categorie categorie; + + public static SimpleSelection fromModel(SelectionModel model) { + if (model == null) + return null; + + return new SimpleSelection.SimpleSelectionBuilder() + .id(model.getId()) + .membre(model.getMembre().getId()) + .saison(model.getSaison()) + .categorie(model.getCategorie()) + .build(); + } +} diff --git a/src/main/webapp/src/pages/MePage.jsx b/src/main/webapp/src/pages/MePage.jsx index aac61d3..89f46bb 100644 --- a/src/main/webapp/src/pages/MePage.jsx +++ b/src/main/webapp/src/pages/MePage.jsx @@ -14,7 +14,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import {CheckField} from "../components/MemberCustomFiels.jsx"; import {toast} from "react-toastify"; -import {apiAxios} from "../utils/Tools.js"; +import {apiAxios, getCatName} from "../utils/Tools.js"; import {useEffect, useState} from "react"; const vite_url = import.meta.env.VITE_URL; @@ -40,7 +40,7 @@ export function MePage() {
- +
@@ -93,10 +93,17 @@ function PhotoCard({data}) { ; } -function SelectCard() { +function SelectCard({userData}) { return
Sélection en équipe de France
+
    + {userData?.selections && userData.selections.sort((a, b) => b.saison - a.saison).map((selection, index) => { + return
    +
    {selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}
    +
    + })} +
; } diff --git a/src/main/webapp/src/pages/admin/club/ClubPage.jsx b/src/main/webapp/src/pages/admin/club/ClubPage.jsx index 096e6b8..aa40e94 100644 --- a/src/main/webapp/src/pages/admin/club/ClubPage.jsx +++ b/src/main/webapp/src/pages/admin/club/ClubPage.jsx @@ -100,7 +100,7 @@ function InformationForm({data}) { } return <> -
+
diff --git a/src/main/webapp/src/pages/admin/club/NewClubPage.jsx b/src/main/webapp/src/pages/admin/club/NewClubPage.jsx index 3202060..cb8f792 100644 --- a/src/main/webapp/src/pages/admin/club/NewClubPage.jsx +++ b/src/main/webapp/src/pages/admin/club/NewClubPage.jsx @@ -60,7 +60,7 @@ function InformationForm() { } return <> - +
Nouveau club
diff --git a/src/main/webapp/src/pages/admin/member/InformationForm.jsx b/src/main/webapp/src/pages/admin/member/InformationForm.jsx index 8ac972e..8943be9 100644 --- a/src/main/webapp/src/pages/admin/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/admin/member/InformationForm.jsx @@ -79,7 +79,7 @@ export function InformationForm({data}) { addPhoto(event, formData, send); } - return + return
Information
diff --git a/src/main/webapp/src/pages/admin/member/MemberPage.jsx b/src/main/webapp/src/pages/admin/member/MemberPage.jsx index 302d9a8..9524d8c 100644 --- a/src/main/webapp/src/pages/admin/member/MemberPage.jsx +++ b/src/main/webapp/src/pages/admin/member/MemberPage.jsx @@ -11,6 +11,7 @@ import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; +import {SelectCard} from "./SelectCard.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -58,7 +59,7 @@ export function MemberPage() {
- +
@@ -94,14 +95,3 @@ function PhotoCard({data}) {
; } - -function SelectCard() { - return <> - /*return
-
Sélection en équipe de France
-
-

Web Design

- -
-
;*/ -} diff --git a/src/main/webapp/src/pages/admin/member/SelectCard.jsx b/src/main/webapp/src/pages/admin/member/SelectCard.jsx new file mode 100644 index 0000000..8e6e571 --- /dev/null +++ b/src/main/webapp/src/pages/admin/member/SelectCard.jsx @@ -0,0 +1,208 @@ +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {useEffect, useReducer, useState} from "react"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons"; +import {AxiosError} from "../../../components/AxiosError.jsx"; +import {apiAxios, CatList, errFormater, getCatName, getSaison} from "../../../utils/Tools.js"; +import {toast} from "react-toastify"; + +function selectionReducer(selections, action) { + switch (action.type) { + case 'ADD': + return [ + ...selections, + action.payload + ] + case 'REMOVE': + return selections.filter(selection => selection.id !== action.payload) + case 'UPDATE_OR_ADD': + const index = selections.findIndex(selection => selection.id === action.payload.id) + if (index === -1) { + return [ + ...selections, + action.payload + ] + } else { + selections[index] = action.payload + return [...selections] + } + case 'SORT': + return selections.sort((a, b) => b.saison - a.saison) + default: + throw new Error() + } +} + +export function SelectCard({userData}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1) + + const [modalSelection, setModal] = useState({id: -1, membre: userData.id}) + const [selections, dispatch] = useReducer(selectionReducer, []) + + useEffect(() => { + if (!data) return + for (const dataKey of data) { + dispatch({type: 'UPDATE_OR_ADD', payload: dataKey}) + } + dispatch({type: 'SORT'}) + }, [data]); + + return
+
+
+
Sélection en équipe de France
+
+ +
+
+
+
+
    + {selections.map((selection, index) => { + return
    +
    {selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}
    + +
    + })} + {error && } +
+
+ + +
; +} + +function sendSelection(event, dispatch) { + event.preventDefault(); + + const formData = new FormData(event.target); + formData.set('selection', event.target.selection?.value?.length > 0 ? event.target.selection?.value : null) + formData.set('categorie', event.target.categorie?.value) + + toast.promise( + apiAxios.post(`/selection/${event.target.membre.value}`, { + id: event.target.id.value, + membre: event.target.membre.value, + saison: event.target.saison.value, + categorie: event.target.categorie.value, + }), + { + pending: "Enregistrement de la séléction en cours", + success: "Séléction enregistrée avec succès 🎉", + error: { + render({data}) { + return errFormater(data, "Échec de l'enregistrement de la séléction") + } + } + } + ).then(data => { + dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) + dispatch({type: 'SORT'}) + }) + +} + +function removeSelection(id, dispatch) { + toast.promise( + apiAxios.delete(`/selection/${id}`), + { + pending: "Suppression de la séléction en cours", + success: "Séléction supprimée avec succès 🎉", + error: { + render({data}) { + return errFormater(data, "Échec de la suppression de la séléction") + } + } + } + ).then(_ => { + dispatch({type: 'REMOVE', payload: id}) + }) +} + +function ModalContent({selection, dispatch}) { + const [saison, setSaison] = useState(0) + const [cat, setCat] = useState("") + const [isNew, setNew] = useState(true) + + const setSeason = (event) => { + setSaison(Number(event.target.value)) + } + const handleCatChange = (event) => { + setCat(event.target.value); + } + + useEffect(() => { + if (selection.id !== -1) { + setNew(false) + setSaison(selection.saison) + } else { + setNew(true) + setSaison(getSaison()) + } + setCat(selection.categorie) + }, [selection]); + + return sendSelection(e, dispatch)}> + + +
+

Edition de la séléction

+ +
+
+
+ {isNew + ? + : <>{saison} + } + - + {saison + 1} +
+ +
+ + +
+ +
+
+ + + {isNew || } +
+ +} + +function RadioGroupeOnOff({value, onChange, name, text}) { + return
+ {text} + + + + +
; +} diff --git a/src/main/webapp/src/pages/club/club/MyClubPage.jsx b/src/main/webapp/src/pages/club/club/MyClubPage.jsx index 523b0c7..baf26cb 100644 --- a/src/main/webapp/src/pages/club/club/MyClubPage.jsx +++ b/src/main/webapp/src/pages/club/club/MyClubPage.jsx @@ -67,7 +67,7 @@ function InformationForm({data}) { } return <> -
+
Affiliation n°{data.no_affiliation}
diff --git a/src/main/webapp/src/pages/club/member/InformationForm.jsx b/src/main/webapp/src/pages/club/member/InformationForm.jsx index 9446f96..787acb0 100644 --- a/src/main/webapp/src/pages/club/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/club/member/InformationForm.jsx @@ -42,7 +42,7 @@ export function InformationForm({data}) { addPhoto(event, formData, send); } - return + return
Information
diff --git a/src/main/webapp/src/pages/club/member/MemberPage.jsx b/src/main/webapp/src/pages/club/member/MemberPage.jsx index 733e4fc..feb59b6 100644 --- a/src/main/webapp/src/pages/club/member/MemberPage.jsx +++ b/src/main/webapp/src/pages/club/member/MemberPage.jsx @@ -10,6 +10,7 @@ import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; +import {SelectCard} from "./SelectCard.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -56,7 +57,7 @@ export function MemberPage() {
- +
{data.licence == null && @@ -93,14 +94,3 @@ function PhotoCard({data}) {
; } - -function SelectCard() { - return <> - /*return
-
Sélection en équipe de France
-
-

Soon

- -
-
;*/ -} diff --git a/src/main/webapp/src/pages/club/member/SelectCard.jsx b/src/main/webapp/src/pages/club/member/SelectCard.jsx new file mode 100644 index 0000000..a65f839 --- /dev/null +++ b/src/main/webapp/src/pages/club/member/SelectCard.jsx @@ -0,0 +1,27 @@ +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {getCatName} from "../../../utils/Tools.js"; +import {AxiosError} from "../../../components/AxiosError.jsx"; + +export function SelectCard({userData}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1) + + return
+
+
+
Sélection en équipe de France
+
+
+
+
    + {data && data.sort((a, b) => b.saison - a.saison).map((selection, index) => { + return
    +
    {selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}
    +
    + })} + {error && } +
+
+
; +} -- 2.49.0