From 4b969e6d6909815c624a632d13444cfab7017098 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 19 Dec 2025 00:30:35 +0100 Subject: [PATCH] feat: add CardPanel --- .../data/repository/CardboardRepository.java | 9 + .../ffsaf/domain/entity/CardboardEntity.java | 5 +- .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 9 +- .../titionfire/ffsaf/ws/recv/RCardboard.java | 128 ++++++++++ .../titionfire/ffsaf/ws/send/SSCardboard.java | 13 ++ .../competition/editor/CMTMatchPanel.css | 5 + .../competition/editor/CMTMatchPanel.jsx | 220 +++++++++++++----- .../src/pages/competition/editor/CMTable.jsx | 14 +- src/main/webapp/src/utils/MatchReducer.jsx | 16 ++ 9 files changed, 349 insertions(+), 70 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java create mode 100644 src/main/webapp/src/pages/competition/editor/CMTMatchPanel.css diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java new file mode 100644 index 0000000..11fc320 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.CardboardModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CardboardRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java index b0eb9c7..ca15a38 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java @@ -17,7 +17,10 @@ public class CardboardEntity { int yellow; public static CardboardEntity fromModel(CardboardModel model) { - return new CardboardEntity(model.getComb().getId(), model.getMatch().getId(), model.getCompet().getId(), + return new CardboardEntity( + model.getComb() != null ? model.getComb().getId() : model.getGuestComb().getId() * -1, + model.getMatch().getId(), + model.getCompet().getId(), model.getRed(), model.getYellow()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index e0cf3bd..c2256d2 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -6,10 +6,7 @@ import fr.titionfire.ffsaf.domain.service.CompetPermService; import fr.titionfire.ffsaf.net2.MessageType; import fr.titionfire.ffsaf.utils.SecurityCtx; import fr.titionfire.ffsaf.ws.data.WelcomeInfo; -import fr.titionfire.ffsaf.ws.recv.RCategorie; -import fr.titionfire.ffsaf.ws.recv.RMatch; -import fr.titionfire.ffsaf.ws.recv.RRegister; -import fr.titionfire.ffsaf.ws.recv.WSReceiver; +import fr.titionfire.ffsaf.ws.recv.*; import fr.titionfire.ffsaf.ws.send.JsonUni; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.security.Authenticated; @@ -44,6 +41,9 @@ public class CompetitionWS { @Inject RRegister rRegister; + @Inject + RCardboard rCardboard; + @Inject SecurityCtx securityCtx; @@ -77,6 +77,7 @@ public class CompetitionWS { getWSReceiverMethods(RMatch.class, rMatch); getWSReceiverMethods(RCategorie.class, rCategorie); getWSReceiverMethods(RRegister.class, rRegister); + getWSReceiverMethods(RCardboard.class, rCardboard); } @OnOpen diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java new file mode 100644 index 0000000..59b6360 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java @@ -0,0 +1,128 @@ +package fr.titionfire.ffsaf.ws.recv; + +import fr.titionfire.ffsaf.data.model.CardboardModel; +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.data.repository.CardboardRepository; +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.domain.entity.CardboardEntity; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.rest.exception.DNotFoundException; +import fr.titionfire.ffsaf.ws.PermLevel; +import fr.titionfire.ffsaf.ws.send.SSCardboard; +import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.Data; + +import java.util.Objects; + +@WithSession +@ApplicationScoped +@RegisterForReflection +public class RCardboard { + + @Inject + MatchRepository matchRepository; + + @Inject + CardboardRepository cardboardRepository; + + private Uni getById(long id, WebSocketConnection connection) { + return matchRepository.findById(id) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException("Matche non trouver"); + if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) + throw new DForbiddenException("Permission denied"); + })); + } + + @WSReceiver(code = "sendCardboardChange", permission = PermLevel.TABLE) + public Uni sendCardboardChange(WebSocketConnection connection, SendCardboard card) { + return getById(card.matchId, connection) + .chain(matchModel -> cardboardRepository.find("(comb.id = ?1 OR guestComb.id = ?2) AND match.id = ?3", + card.combId, card.combId * -1, card.matchId).firstResult() + .chain(model -> { + if (model != null) { + model.setRed(model.getRed() + card.red); + model.setYellow(model.getYellow() + card.yellow); + return Panache.withTransaction(() -> cardboardRepository.persist(model)); + } + CardboardModel cardboardModel = new CardboardModel(); + + cardboardModel.setCompet(matchModel.getCategory().getCompet()); + cardboardModel.setMatch(matchModel); + cardboardModel.setRed(card.red); + cardboardModel.setYellow(card.yellow); + cardboardModel.setComb(null); + cardboardModel.setGuestComb(null); + + if (card.combId >= 0) { + if (matchModel.getC1_id() != null && matchModel.getC1_id().getId() == card.combId) + cardboardModel.setComb(matchModel.getC1_id()); + if (matchModel.getC2_id() != null && matchModel.getC2_id().getId() == card.combId) + cardboardModel.setComb(matchModel.getC2_id()); + } else { + if (matchModel.getC1_guest() != null && matchModel.getC1_guest() + .getId() == card.combId * -1) + cardboardModel.setGuestComb(matchModel.getC1_guest()); + if (matchModel.getC2_guest() != null && matchModel.getC2_guest() + .getId() == card.combId * -1) + cardboardModel.setGuestComb(matchModel.getC2_guest()); + } + + if (cardboardModel.getComb() == null && cardboardModel.getGuestComb() == null) + return Uni.createFrom().nullItem(); + return Panache.withTransaction(() -> cardboardRepository.persist(cardboardModel)); + })) + .call(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model))) + .replaceWithVoid(); + } + + @WSReceiver(code = "getCardboardWithoutThis", permission = PermLevel.VIEW) + public Uni getCardboardWithoutThis(WebSocketConnection connection, Long matchId) { + return getById(matchId, connection) + .chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2", matchModel.getCategory().getCompet(), matchModel) + .map(models -> { + CardboardAllMatch out = new CardboardAllMatch(); + + models.stream().filter(c -> (matchModel.getC1_id() != null + && Objects.equals(c.getComb(), matchModel.getC1_id())) + || (matchModel.getC1_guest() != null + && Objects.equals(c.getGuestComb(), matchModel.getC1_guest()))) + .forEach(c -> { + out.c1_yellow += c.getYellow(); + out.c1_red += c.getRed(); + }); + + models.stream().filter(c -> (matchModel.getC2_id() != null + && Objects.equals(c.getComb(), matchModel.getC2_id())) + || (matchModel.getC2_guest() != null + && Objects.equals(c.getGuestComb(), matchModel.getC2_guest()))) + .forEach(c -> { + out.c2_yellow += c.getYellow(); + out.c2_red += c.getRed(); + }); + + return out; + })); + } + + @RegisterForReflection + public record SendCardboard(long matchId, long combId, int yellow, int red) { + } + + @Data + @RegisterForReflection + public static class CardboardAllMatch { + int c1_yellow = 0; + int c1_red = 0; + int c2_yellow = 0; + int c2_red = 0; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java new file mode 100644 index 0000000..31963b1 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java @@ -0,0 +1,13 @@ +package fr.titionfire.ffsaf.ws.send; + +import fr.titionfire.ffsaf.domain.entity.CardboardEntity; +import fr.titionfire.ffsaf.ws.CompetitionWS; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Uni; + +public class SSCardboard { + + public static Uni sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity); + } +} diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.css b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.css new file mode 100644 index 0000000..472e94d --- /dev/null +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.css @@ -0,0 +1,5 @@ +.btn-xs { + --bs-btn-padding-y: .05rem; + --bs-btn-padding-x: .6rem; + --bs-btn-font-size: .75rem; +} diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index aca21fc..4b8e048 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -10,6 +10,7 @@ import {scorePrint, win} from "../../../utils/Tools.js"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {toast} from "react-toastify"; +import "./CMTMatchPanel.css" function CupImg() { return { + reducer({type: 'UPDATE_CARDBOARD', payload: {...data}}) + } + dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}}) dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}}) dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}}) dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}}) + dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}}) return () => { dispatch({type: 'removeListener', payload: treeListener}) dispatch({type: 'removeListener', payload: matchListener}) dispatch({type: 'removeListener', payload: matchOrder}) dispatch({type: 'removeListener', payload: deleteMatch}) + dispatch({type: 'removeListener', payload: sendCardboard}) } }, [catId]); @@ -164,6 +171,7 @@ function MatchList({matches, cat, menuActions}) { const marches2 = matches.filter(m => m.categorie_ord !== -42) .sort((a, b) => a.categorie_ord - b.categorie_ord) .map(m => ({...m, win: win(m.scores)})) + const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; const match = matches.find(m => m.id === activeMatch) useEffect(() => { @@ -175,7 +183,10 @@ function MatchList({matches, cat, menuActions}) { payload: { c1: match.c1, c2: match.c2, - next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2})) + next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({ + c1: m.c1, + c2: m.c2 + })) } }); } @@ -187,7 +198,7 @@ function MatchList({matches, cat, menuActions}) { useEffect(() => { if (match && match.poule !== lice) - setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id) + setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id) }, [lice]); useEffect(() => { @@ -196,10 +207,8 @@ function MatchList({matches, cat, menuActions}) { if (marches2.some(m => m.id === activeMatch)) return; - setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id); + setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id); }, [matches]) - - const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; return <> {liceName.length > 1 &&
@@ -230,7 +239,8 @@ function MatchList({matches, cat, menuActions}) { {marches2.map((m, index) => ( - setActiveMatch(m.id)}> {liceName[(index - firstIndex) % liceName.length]} @@ -326,6 +336,16 @@ function BuildTree({treeData, matches, menuActions}) { } function ScorePanel({matchId, match, menuActions}) { + const onClickVoid = useRef(() => { + }); + + return
+ + +
+} + +function ScorePanel_({matchId, match, menuActions, onClickVoid_}) { const {sendRequest} = useWS() const setLoading = useLoadingSwitcher() @@ -418,6 +438,7 @@ function ScorePanel({matchId, match, menuActions}) { sel.style.display = "none"; lastScoreClick.current = null; } + onClickVoid_.current = onClickVoid; useEffect(() => { if (!match || match?.end === end) @@ -463,64 +484,143 @@ function ScorePanel({matchId, match, menuActions}) { "-999 : forfait" const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0; - return
-
-
Scores
- - - - - - + return
+
Scores
+
MancheRougeBleu
+ + + + + + + + + {match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => ( + + + + - - - {match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => ( - - - - - - ))} - - - - - - -
MancheRougeBleu
{score.n_round + 1} scoreRef.current[score.n_round * 2] = e} + onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)} scoreRef.current[score.n_round * 2 + 1] = e} + onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}
{score.n_round + 1} scoreRef.current[score.n_round * 2] = e} - onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)} scoreRef.current[score.n_round * 2 + 1] = e} - onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}
scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>- scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>- -
-
-
- setEnd(e.target.checked)}/> - + ))} + + + scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>- + scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>- + + + + +
+
+ setEnd(e.target.checked)}/> + +
+
+ setScoreIn(e.target.value)} + onClick={e => e.stopPropagation()} + onKeyDown={e => { + if (e.key === "Tab") { + if (lastScoreClick.current !== null) { + const {round, comb} = lastScoreClick.current; + const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1); + if (nextIndex >= 0 && nextIndex < scoreRef.current.length) { + e.preventDefault(); + scoreRef.current[nextIndex].click(); + } + } + } else if (e.key === "Enter") { + e.preventDefault(); + onClickVoid(); + } + }}/> +
+} + +function CardPanel({matchId, match}) { + const {sendRequest, dispatch} = useWS(); + const setLoading = useLoadingSwitcher() + + const {data, refresh} = useRequestWS('getCardboardWithoutThis', matchId, setLoading); + + useEffect(() => { + refresh('getCardboardWithoutThis', matchId); + + const sendCardboard = ({data}) => { + if (data.comb_id === match.c1 || data.comb_id === match.c2) { + refresh('getCardboardWithoutThis', matchId); + } + } + + dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}}) + return () => dispatch({type: 'removeListener', payload: sendCardboard}) + }, [matchId]) + + if (!match) { + return
+ } + + const c1Cards = match.cardboard?.find(c => c.comb_id === match.c1) || {red: 0, yellow: 0}; + const c2Cards = match.cardboard?.find(c => c.comb_id === match.c2) || {red: 0, yellow: 0}; + + const handleCard = (combId, yellow, red) => { + if (combId === match.c1) { + if (c1Cards.red + red < 0 || c1Cards.yellow + yellow < 0) + return; + } else if (combId === match.c2) { + if (c2Cards.red + red < 0 || c2Cards.yellow + yellow < 0) + return; + } else { + return; + } + + setLoading(1) + sendRequest('sendCardboardChange', {matchId, combId, yellow, red}) + .finally(() => { + setLoading(0) + }) + } + + return
+
Carton
+
+
Competition: {(data?.c1_red || 0) + c1Cards.red} {(data?.c1_yellow || 0) + c1Cards.yellow}
+
+ Match: +
+ + {c1Cards.red} + +
+
+ + {c1Cards.yellow} +
- setScoreIn(e.target.value)} - onClick={e => e.stopPropagation()} - onKeyDown={e => { - if (e.key === "Tab") { - if (lastScoreClick.current !== null) { - const {round, comb} = lastScoreClick.current; - const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1); - if (nextIndex >= 0 && nextIndex < scoreRef.current.length) { - e.preventDefault(); - scoreRef.current[nextIndex].click(); - } - } - } else if (e.key === "Enter") { - e.preventDefault(); - onClickVoid(); - } - }}/>
-
+
+
Competition: {(data?.c2_red || 0) + c2Cards.red} {(data?.c2_yellow || 0) + c2Cards.yellow}
+
+ Match: +
+ + {c2Cards.red} + +
+
+ + {c2Cards.yellow} + +
+
} diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 8eb8db2..e9e2ddb 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -61,6 +61,8 @@ export function CMTable() { const windowName = "FFSAFScorePublicWindow"; +let tto = []; + function Menu({menuActions}) { const e = document.getElementById("actionMenu") const publicAffDispatch = usePubAffDispatch() @@ -102,6 +104,11 @@ function Menu({menuActions}) { } } + for (const x of tto) + x.dispose(); + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]') + tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) + const handleScore = __ => { setShowScore(!showScore); publicAffDispatch({type: 'SET_DATA', payload: {showScore: !showScore}}); @@ -111,10 +118,6 @@ function Menu({menuActions}) { menuActions.current.switchSore?.(); } - const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]') - const o = [...tooltipTriggerList] - o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) - if (!e) return <>; return <> @@ -122,7 +125,8 @@ function Menu({menuActions}) { <>
+ data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Inverser la position des combattants sur cette écran"/>
data.id === action.payload.match_id) + if (idx === -1) + return datas // Do nothing + const data = datas[idx] + const tmp = data.cardboard?.find(c => c.comb_id === action.payload.comb_id) + if (tmp) { + tmp.red = action.payload.red + tmp.yellow = action.payload.yellow + } else { + if (!data.cardboard) + data.cardboard = [] + data.cardboard.push(action.payload) + } + return [...datas] case 'SORT': return datas.sort(action.payload) case 'REORDER':