From 2ccd2a9178fb850dca753fae22afca21d98b9805 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sat, 27 Dec 2025 13:02:45 +0100 Subject: [PATCH 01/12] fix: disable match filter on 1 zone cat + rename lice to zone --- .../src/pages/competition/editor/CMAdmin.jsx | 12 ++++++------ .../pages/competition/editor/CMTMatchPanel.jsx | 18 +++++++++++------- .../editor/CategoryAdminContent.jsx | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 8c56854..54b7546 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -98,7 +98,7 @@ function CategoryHeader({cat, setCatId}) {
{cat &&
Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} | - Lice: {cat.liceName}
} + Zone: {cat.liceName}
}
- - Nom des zones de combat (séparée par des ';') + setLice(e.target.value)}/>
diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 4b8e048..3d88ca2 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -173,6 +173,10 @@ function MatchList({matches, cat, menuActions}) { .map(m => ({...m, win: win(m.scores)})) const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; + const isActiveMatch = (index) => { + return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice) + } + const match = matches.find(m => m.id === activeMatch) useEffect(() => { if (!match) { @@ -183,7 +187,7 @@ function MatchList({matches, cat, menuActions}) { payload: { c1: match.c1, c2: match.c2, - next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({ + next: marches2.filter((m, index) => !m.end && isActiveMatch(index) && m.id !== activeMatch).map(m => ({ c1: m.c1, c2: m.c2 })) @@ -198,7 +202,7 @@ function MatchList({matches, cat, menuActions}) { useEffect(() => { if (match && match.poule !== lice) - setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id) + setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id) }, [lice]); useEffect(() => { @@ -207,12 +211,12 @@ function MatchList({matches, cat, menuActions}) { if (marches2.some(m => m.id === activeMatch)) return; - setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id); + setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id); }, [matches]) return <> {liceName.length > 1 && -
- +
+ + + / +
+
+ Mot de passe du serveur + +
+
+ Dossier des resources + +
+
+
+ + +
+ + + + + + } diff --git a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx index 40ac209..2c744b5 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx @@ -9,7 +9,7 @@ export function ChronoPanel() { }) const [chrono, setChrono] = useState({time: 0, startTime: 0}) const chronoText = useRef(null) - const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"}) + const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"}) const publicAffDispatch = usePubAffDispatch(); const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time})) @@ -34,12 +34,12 @@ export function ChronoPanel() { const timer = setInterval(() => { let currentDuration = config.time - let color = "black" + let color = "#000000" if (state_.chronoState === 1) { - color = (state_.countBlink < blinkRfDuration) ? "black" : "red" + color = (state_.countBlink < blinkRfDuration) ? "#000000" : "#ff0000" } else if (state_.chronoState === 2) { currentDuration = (state_.chronoState === 0) ? 10000 : config.pause - color = (state_.countBlink < blinkRfDuration) ? "green" : "red" + color = (state_.countBlink < blinkRfDuration) ? "#008000" : "#ff0000" } const timeStr = timePrint(currentDuration - getTime()) diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index d6f2966..a4522be 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -11,6 +11,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {toast} from "react-toastify"; import "./CMTMatchPanel.css" +import {useOBS} from "../../../hooks/useOBS.jsx"; function CupImg() { return { const categoryListener = ({data}) => { @@ -47,6 +49,10 @@ export function CategorieSelect({catId, setCatId, menuActions}) { const cat = cats?.find(c => c.id === catId); + useEffect(() => { + setText("poule",cat ? cat.name : ""); + }, [cat, connected]); + return <>
Catégorie
diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 1828795..287b77e 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -1,16 +1,20 @@ import React, {useEffect, useRef, useState} from "react"; import {useRequestWS} from "../../../hooks/useWS.jsx"; -import {useCombsDispatch} from "../../../hooks/useComb.jsx"; +import {useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {createPortal} from "react-dom"; import {copyStyles} from "../../../utils/copyStyles.js"; -import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; +import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx"; import {faArrowRightArrowLeft, faDisplay} from "@fortawesome/free-solid-svg-icons"; import {PubAffWindow} from "./PubAffWindow.jsx"; import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts"; import {ChronoPanel} from "./CMTChronoPanel.jsx"; 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() { const combDispatch = useCombsDispatch() @@ -24,36 +28,39 @@ export function CMTable() { combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); }, [data]); - return -
-
-
-
-
Chronomètre
-
- + return + +
+
+
+
+
Chronomètre
+
+ +
-
-
-
Score
-
- -
-
-
-
-
-
Matches
-
- +
+
Score
+
+ +
+
+
+
+
+
Matches
+
+ +
+ +
- -
-
+ +
} const windowName = "FFSAFScorePublicWindow"; @@ -65,6 +72,9 @@ function Menu({menuActions}) { const publicAffDispatch = usePubAffDispatch() const [showPubAff, setShowPubAff] = useState(false) const [showScore, setShowScore] = useState(true) + const {connected, connect, disconnect} = useOBS(); + const longPress = useRef({time: null, timer: null, button: null}); + const obsModal = useRef(null); const externalWindow = useRef(null) const containerEl = useRef(document.createElement("div")) @@ -73,11 +83,6 @@ function Menu({menuActions}) { if (sessionStorage.getItem(windowName + "_open") === "true") { handlePubAff(); } - //return () => { - // if (!externalWindow.current) - // return; - // externalWindow.current.close(); - //} }, []); const handlePubAff = __ => { @@ -115,15 +120,76 @@ function Menu({menuActions}) { menuActions.current.switchSore?.(); } + const longTimeAction = (button) => { + if (button === "obs") { + obsModal.current.click(); + } + } + + const longPressDown = (button) => { + longPress.current.button = button; + longPress.current.time = new Date(); + longPress.current.timer = setTimeout(() => { + longTimeAction(button); + + longPress.current.time = null; + longPress.current.button = null; + }, 1000); + } + + const longPressUp = (button) => { + clearTimeout(longPress.current.timer); + + if (longPress.current.time) { + const diff = new Date() - longPress.current.time; + if (longPress.current.button === button) { + if (diff >= 1000) { + longTimeAction(button); + } else { + if (button === "obs") { + if (connected) { + disconnect(); + } else { + importOBSConfiguration() + .then(config => { + connect("ws://" + config.adresse + "/", config.password, config.assets_dir); + }) + .catch(() => { + toast.error("Aucune configuration OBS trouvée, veuillez en importer une"); + }); + } + } + } + } + + longPress.current.time = null; + longPress.current.button = null; + } + } + + const handleOBSSubmit = (e) => { + e.preventDefault(); + const form = e.target; + const prefix = form[0].value; + + sessionStorage.setItem("obs_prefix", prefix); + } + if (!e) return <>; return <> {createPortal( <>
- + longPressDown("obs")} + onMouseUp={() => longPressUp("obs")} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice"/>
, document.getElementById("actionMenu"))} {externalWindow.current && createPortal(, containerEl.current)} + + + } + +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 {getComb} = useCombs(); + + useEffect(() => { + if (state.c1 !== oldState.current.c1) { + 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`) + setDiapo("img.rouge", files); + oldState.current.c1 = state.c1; + } + + if (state.c2 !== oldState.current.c2) { + 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`) + setDiapo("img.blue", files); + oldState.current.c2 = state.c2; + } + + if (state.showScore !== oldState.current.showScore) { + setText("score.rouge", state.showScore ? state.scoreRouge.toString() : ""); + setText("score.blue", state.showScore ? state.scoreBleu.toString() : ""); + oldState.current.showScore = state.showScore; + } + + if (state.showScore === undefined || state.showScore) { + if (state.scoreRouge !== oldState.current.scoreRouge) { + setText("score.rouge", (state.scoreRouge || 0).toString()); + oldState.current.scoreRouge = state.scoreRouge; + } + if (state.scoreBleu !== oldState.current.scoreBleu) { + setText("score.blue", (state.scoreBleu || 0).toString()); + oldState.current.scoreBleu = state.scoreBleu; + } + } + }, [state]); + + state.timeCb2 = (payload) => { + if (payload.timeStr && payload.timeColor) { + setTextAndColor("temps", payload.timeStr, payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor); + + oldState.current.timeStr = payload.timeStr; + oldState.current.lastColor = payload.timeColor; + } + } + + useEffect(() => { + if (!connected) + return; + // Initial sync + const comb = getComb(oldState.current.c1); + const comb2 = getComb(oldState.current.c2); + setText("comb.rouge", comb ? (comb?.fname + " " + comb?.lname) : ""); + setText("comb.blue", comb2 ? (comb2?.fname + " " + comb2?.lname) : ""); + setTextAndColor("temps", oldState.current.timeStr, oldState.current.timeColor === "#000000" ? "#ffffff" : oldState.current.timeColor); + setText("score.rouge", oldState.current.showScore === undefined || oldState.current.showScore ? oldState.current.scoreRouge.toString() : ""); + setText("score.blue", oldState.current.showScore === undefined || oldState.current.showScore ? oldState.current.scoreBleu.toString() : ""); + + }, [connected]) +} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 73d349f..faae064 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -17,13 +17,15 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faTrash} from "@fortawesome/free-solid-svg-icons"; import {win} from "../../../utils/Tools.js"; +const vite_url = import.meta.env.VITE_URL; + function CupImg() { return } -export function CategoryContent({cat, catId, setCat}) { +export function CategoryContent({cat, catId, setCat, menuActions}) { const setLoading = useLoadingSwitcher() const {sendRequest, dispatch} = useWS(); const [matches, reducer] = useReducer(MarchReducer, []); @@ -148,7 +150,7 @@ export function CategoryContent({cat, catId, setCat}) { return <>
- +
{cat && } @@ -156,7 +158,7 @@ export function CategoryContent({cat, catId, setCat}) { } -function AddComb({groups, setGroups, removeGroup}) { +function AddComb({groups, setGroups, removeGroup, menuActions}) { const {data, setData} = useRequestWS("getRegister", null) const combDispatch = useCombsDispatch() const {dispatch} = useWS() @@ -190,6 +192,21 @@ function AddComb({groups, setGroups, removeGroup}) { if (data === null) return; combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); + + const resourceList = [] + data.forEach(d => { + if (d.club_uuid) { + const url = `${vite_url}/api/club/${d.club_uuid}/logo`; + if (!resourceList.some(d => d.url === url)) + resourceList.push({url: url, name: `club_${d.club_uuid}.png`}); + } + if (d.country) { + const url = `/flags/svg/${d.country.toLowerCase()}.svg`; + if (!resourceList.some(d => d.url === url)) + resourceList.push({url: url, name: `flag_${d.country.toLowerCase()}.svg`}); + } + }) + menuActions.current.resourceList = resourceList }, [data]); return <> diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx index 8eac0bc..0b7273d 100644 --- a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -14,16 +14,16 @@ const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"}; export function PubAffWindow({document}) { const chronoText = useRef(null) - const state2 = useRef({lastColor: "white", lastTimeStr: "--:--"}) + const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"}) const state = usePubAffState(); - document.title = "A React portal window" + document.title = "Affichage Public"; document.body.className = "bg-dark text-white overflow-hidden"; state.timeCb = (payload) => { - state2.current = {lastColor: payload.timeColor === "black" ? "white" : payload.timeColor, lastTimeStr: payload.timeStr} + state2.current = {lastColor: payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor, lastTimeStr: payload.timeStr} chronoText.current.textContent = payload.timeStr - chronoText.current.style.color = payload.timeColor === "black" ? "white" : payload.timeColor + chronoText.current.style.color = payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor } const showScore = state.showScore ?? true; diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 5821030..a74a8e3 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -153,3 +153,31 @@ export function timePrint(time, negSign = false) { String(min).padStart(2, '0') + ":" + String(sec).padStart(2, '0') } + +//create full hex +function fullHex (hex) { + let r = hex.slice(1,2); + let g = hex.slice(2,3); + let b = hex.slice(3,4); + + r = parseInt(r+r, 16); + g = parseInt(g+g, 16); + b = parseInt(b+b, 16); + + // return {r, g, b} + return { r, g, b }; +} + +//convert hex to rgb +export function hex2rgb (hex) { + if(hex.length === 4){ + return fullHex(hex); + } + + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + + // return {r, g, b} + return { r, g, b }; +} -- 2.49.0 From 145d5e7ca8962bf669265efbcffeb09108bc51c5 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 29 Dec 2025 15:41:13 +0100 Subject: [PATCH 11/12] feat: update quarkus --- pom.xml | 6 +++--- .../ffsaf/domain/service/HelloAssoTokenService.java | 2 -- .../fr/titionfire/ffsaf/domain/service/LoggerService.java | 2 ++ .../fr/titionfire/ffsaf/domain/service/MembreService.java | 2 +- src/main/resources/application.properties | 2 -- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 2c0622e..de66203 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 quarkus-bom io.quarkus.platform - 3.16.4 + 3.30.5 true 3.2.3 @@ -68,7 +68,7 @@ io.quarkiverse.tika quarkus-tika - 2.0.4 + 2.3.0 @@ -127,7 +127,7 @@ org.apache.xmlgraphics fop - 2.6 + 2.11 io.quarkus diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java index 253a071..1a5b131 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java @@ -4,7 +4,6 @@ import fr.titionfire.ffsaf.rest.client.HelloAssoAuthClient; import fr.titionfire.ffsaf.rest.client.dto.TokenResponse; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.logging.Logger; @@ -13,7 +12,6 @@ import org.jboss.logging.Logger; public class HelloAssoTokenService { private static final Logger LOG = Logger.getLogger(HelloAssoTokenService.class); - @Inject @RestClient HelloAssoAuthClient authClient; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java index 025c88b..5638a7f 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java @@ -58,6 +58,8 @@ public class LoggerService { } public Uni append() { + if (buffer.isEmpty()) + return Uni.createFrom().voidItem(); return Panache.withTransaction(() -> repository.persist(buffer)) .invoke(__ -> buffer.clear()); } 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 a49428c..023b543 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -158,7 +158,7 @@ public class MembreService { "EXISTS (SELECT 1 FROM LicenceModel l WHERE l.membre.id = m.id AND l.saison >= %s)", Utils.getSaison() - 1); - String clubFilter = "?3 = ?3"; + String clubFilter = "(TRUE OR ?3 = ?3)"; if (club != null) { if (club instanceof String club_) { if (!club_.isBlank()) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0d7d3e6..5ffd4f2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -75,6 +75,4 @@ helloasso.client-id=changeme helloasso.client-secret=changeme quarkus.rest-client.helloasso-auth.url=${helloasso.api}/oauth2 -quarkus.rest-client.helloasso-auth.scope=javax.inject.Singleton - quarkus.rest-client.helloasso-api.url=${helloasso.api}/v5 -- 2.49.0 From bf377c9c5e273e9e70a8518d382810d3244e190e Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 29 Dec 2025 21:03:05 +0100 Subject: [PATCH 12/12] feat: external website result connector --- .../ffsaf/data/model/RegisterModel.java | 9 + .../ffsaf/domain/service/ResultService.java | 485 ++++++--- .../ffsaf/domain/service/UpdateService.java | 23 + .../ffsaf/rest/ExternalResultEndpoints.java | 85 ++ .../ffsaf/rest/data/ResultCategoryData.java | 2 + .../java/fr/titionfire/ffsaf/utils/Utils.java | 14 + src/main/webapp/.gitignore | 1 + src/main/webapp/public/competition.js | 946 ++++++++++++++++++ .../src/pages/competition/editor/CMAdmin.jsx | 26 +- .../editor/CompetitionManagerRoot.jsx | 2 +- 10 files changed, 1440 insertions(+), 153 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java create mode 100644 src/main/webapp/public/competition.js diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 02c0f90..16691d3 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -66,4 +66,13 @@ public class RegisterModel { return membre.club; return club; } + + public Categorie getCategorie2() { + Categorie tmp = this.categorie; + if (tmp == null) + tmp = membre.getCategorie(); + if (tmp == null) + return null; + return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)]; + } } 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 ecd48a4..e83009c 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -1,10 +1,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.*; -import fr.titionfire.ffsaf.data.repository.CategoryRepository; -import fr.titionfire.ffsaf.data.repository.CompetitionRepository; -import fr.titionfire.ffsaf.data.repository.MatchRepository; -import fr.titionfire.ffsaf.data.repository.RegisterRepository; +import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.rest.data.ResultCategoryData; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.utils.*; @@ -19,7 +16,9 @@ import lombok.Builder; import org.hibernate.reactive.mutiny.Mutiny; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; @WithSession @@ -35,6 +34,12 @@ public class ResultService { @Inject MembreService membreService; + @Inject + ClubRepository clubRepository; + + @Inject + CompetitionGuestRepository competitionGuestRepository; + @Inject CategoryRepository categoryRepository; @@ -53,10 +58,22 @@ public class ResultService { .collect().asList(); } + public Uni> getCategoryList(String uuid) { + return categoryRepository.list("compet.uuid = ?1", uuid) + .map(categoryModels -> { + HashMap map = new HashMap<>(); + categoryModels.stream() + .sorted(Comparator.comparing(CategoryModel::getName)) + .forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId())); + return map; + }); + } + public Uni> getCategory(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) .chain(m -> categoryRepository.list("compet.uuid = ?1", uuid) - .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True + .chain(cats -> matchRepository.list( + "(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True m.getMembre(), cats))) .map(matchModels -> { HashMap> map = new HashMap<>(); @@ -82,6 +99,8 @@ public class ResultService { CategoryModel categoryModel = matchModels.get(0).getCategory(); out.setName(categoryModel.getName()); out.setType(categoryModel.getType()); + out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";")); + out.setGenTime(System.currentTimeMillis()); getArray2(matchModels, out); getTree(categoryModel.getTree(), out); @@ -173,6 +192,12 @@ public class ResultService { } } + public Uni getCategoryPublic(String uuid, long poule) { + return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule) + .call(list -> Mutiny.fetch(list.get(0).getCategory().getTree())) + .map(this::getData); + } + private void getTree(List treeModels, ResultCategoryData out) { ArrayList> trees = new ArrayList<>(); treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> { @@ -185,87 +210,230 @@ public class ResultService { public Uni getAllCombArray(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) - .chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) - .map(pair -> { - List registers = pair.getKey(); - List matchModels = pair.getValue(); + .chain(__ -> getAllCombArray(uuid)); + } - CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); + public Uni getAllCombArrayPublic(String uuid) { + return getAllCombArray(uuid); + } - List combs = matchModels.stream() - .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) - .filter(Objects::nonNull) - .distinct() - .map(combName -> { - var builder2 = CombsArrayData.CombsData.builder(); - AtomicInteger w = new AtomicInteger(0); - AtomicInteger l = new AtomicInteger(0); - AtomicInteger pointMake = new AtomicInteger(); - AtomicInteger pointTake = new AtomicInteger(); + public Uni getAllCombArray(String uuid) { + return registerRepository.list("competition.uuid = ?1", uuid) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); - matchModels.stream() - .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) - || m.getC2Name().equals(combName))) - .forEach(matchModel -> { - int win = matchModel.win(); - if ((combName.equals(matchModel.getC1Name()) && win > 0) || - combName.equals(matchModel.getC2Name()) && win < 0) { - w.getAndIncrement(); - } else { - l.getAndIncrement(); - } + CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) - .forEach(score -> { - if (combName.equals(matchModel.getC1Name())) { - pointMake.addAndGet(score.getS1()); - pointTake.addAndGet(score.getS2()); - } else { - pointMake.addAndGet(score.getS2()); - pointTake.addAndGet(score.getS1()); - } - }); - }); + List combs = matchModels.stream() + .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) + .filter(Objects::nonNull) + .distinct() + .map(combName -> { + var builder2 = CombsArrayData.CombsData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); - Categorie categorie = null; - ClubModel club = null; + matchModels.stream() + .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) + || m.getC2Name().equals(combName))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((combName.equals(matchModel.getC1Name()) && win > 0) || + combName.equals(matchModel.getC2Name()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } - Optional register = registers.stream() - .filter(r -> r.getName().equals(combName)).findFirst(); - if (register.isPresent()) { - categorie = register.get().getCategorie(); - club = register.get().getClub2(); - } + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (combName.equals(matchModel.getC1Name())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); - builder2.name(combName); - builder2.w(w.get()); - builder2.l(l.get()); - builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); - builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); - builder2.pointMake(pointMake.get()); - builder2.pointTake(pointTake.get()); - builder2.ratioPoint( - (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + Categorie categorie = null; + ClubModel club = null; - return builder2.build(); - }) - .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) - .toList(); + Optional register = registers.stream() + .filter(r -> r.getName().equals(combName)).findFirst(); + if (register.isPresent()) { + categorie = register.get().getCategorie(); + club = register.get().getClub2(); + } - builder.nb_insc(combs.size()); - builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); - builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); - builder.combs(combs); + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(combName); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); - return builder.build(); + return builder2.build(); + }) + .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) + .toList(); + + builder.nb_insc(combs.size()); + builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); + builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); + builder.combs(combs); + + return builder.build(); + }); + } + + public Uni> getCombList(String uuid) { + return registerRepository.list("competition.uuid = ?1", uuid) + .map(models -> { + HashMap map = new HashMap<>(); + models.forEach(registerEmbeddable -> { + map.put(Utils.getFullName(registerEmbeddable.getMembre()), + registerEmbeddable.getMembre().getFname() + "¤" + registerEmbeddable.getMembre() + .getLname()); + }); + return map; + }) + .chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid) + .map(models -> { + models.forEach(guestModel -> map.put(Utils.getFullName(guestModel), + guestModel.getFname() + "¤" + guestModel.getLname())); + return map; }) ); } + public Uni getCombArrayPublic(String uuid, String fname, String lname) { + CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder(); + AtomicBoolean guest = new AtomicBoolean(false); + AtomicLong id = new AtomicLong(0); + + + return registerRepository.find("membre.fname = ?1 AND membre.lname = ?2 AND competition.uuid = ?3", fname, + lname, uuid).firstResult() + .chain(registerModel -> { + if (registerModel == null) { + return competitionGuestRepository.find("fname = ?1 AND lname = ?2 AND competition.uuid = ?3", + fname, lname, uuid).firstResult() + .chain(guestModel -> { + builder.name(Utils.getFullName(guestModel)); + builder.club(guestModel.getClub()); + guest.set(true); + id.set(guestModel.getId()); + builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie() + .getName(BUNDLE)); + + return matchRepository.list( + "category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)", uuid, + guestModel); + }); + } + builder.name(Utils.getFullName(registerModel.getMembre())); + builder.club((registerModel.getClub2() == null) ? BUNDLE.getString( + "no.licence") : registerModel.getClub2().getName()); + id.set(registerModel.getMembre().getId()); + builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2() + .getName(BUNDLE)); + + return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid, + registerModel.getMembre()); + }) + .invoke(matchModels -> { + List pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct() + .toList(); + List matchs = new ArrayList<>(); + + AtomicInteger sumW = new AtomicInteger(); + AtomicInteger sumPointMake = new AtomicInteger(0); + AtomicInteger sumPointTake = new AtomicInteger(0); + + for (MatchModel matchModel : matchModels) { + if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) || + (matchModel.getC2_id() == null && matchModel.getC2_guest() == null)) + continue; + + var builder2 = CombArrayData.MatchsData.builder(); + builder2.date(matchModel.getDate()); + builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory())) + .map(CategoryModel::getName).findFirst().orElse("")); + + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + if ((!guest.get() && matchModel.getC1_id() != null && matchModel.getC1_id().getId() == id.get()) + || (guest.get() && matchModel.getC1_guest() != null && matchModel.getC1_guest() + .getId() == id.get())) { + builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest())); + if (matchModel.isEnd()) { + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(scoreEntity -> { + pointMake.addAndGet(scoreEntity.getS1()); + pointTake.addAndGet(scoreEntity.getS2()); + }); + builder2.score(matchModel.getScores().stream() + .map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()); + } else { + builder2.score(new ArrayList<>()); + } + builder2.win(matchModel.win() > 0); + } else { + builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest())); + if (matchModel.isEnd()) { + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(scoreEntity -> { + pointMake.addAndGet(scoreEntity.getS2()); + pointTake.addAndGet(scoreEntity.getS1()); + }); + builder2.score(matchModel.getScores().stream() + .map(s -> new Integer[]{s.getS2(), s.getS1()}).toList()); + } else { + builder2.score(new ArrayList<>()); + } + builder2.win(matchModel.win() < 0); + } + + builder2.ratio( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + + sumPointMake.addAndGet(pointMake.get()); + sumPointTake.addAndGet(pointTake.get()); + + matchs.add(builder2.build()); + if (builder2.win) + sumW.getAndIncrement(); + } + + builder.totalWin(sumW.get()); + builder.pointRatio( + (sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get()); + builder.pointMake(sumPointMake.get()); + builder.pointTake(sumPointTake.get()); + + matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule) + .thenComparing(CombArrayData.MatchsData::adv)); + + builder.matchs(matchs); + }) + .map(__ -> builder.build()); + } + @Builder @RegisterForReflection @@ -277,96 +445,117 @@ public class ResultService { } } + @Builder + @RegisterForReflection + public static record CombArrayData(String name, String club, String cat, int totalWin, + float pointRatio, int pointMake, int pointTake, List matchs) { + @Builder + @RegisterForReflection + public static record MatchsData(Date date, String poule, String adv, List score, float ratio, + boolean win) { + } + } + + public Uni> getClubList(String uuid) { // TODO add guest club + return registerRepository.list("competition.uuid = ?1", uuid) + .map(registers -> { + HashMap registerMap = new HashMap<>(); + registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull) + .forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId())); + return registerMap; + }); + } + public Uni getClubArray(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx).chain(cm_register -> getClubArray(uuid, cm_register.getClub2())); + } + + public Uni getClubArrayPublic(String uuid, Long id) { + return clubRepository.findById(id).chain(clubModel -> getClubArray(uuid, clubModel)); + } + + public Uni getClubArray(String uuid, ClubModel clubModel) { ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder(); + builder.name(clubModel.getName()); - return hasAccess(uuid, securityCtx) - .invoke(cm_register -> builder.name(cm_register.getClub2().getName())) - .chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, - cm_register.getClub2()) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) - .map(pair -> { - List registers = pair.getKey(); - List matchModels = pair.getValue(); + return registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); - builder.nb_insc(registers.size()); + builder.nb_insc(registers.size()); - AtomicInteger tt_win = new AtomicInteger(0); - AtomicInteger tt_match = new AtomicInteger(0); + AtomicInteger tt_win = new AtomicInteger(0); + AtomicInteger tt_match = new AtomicInteger(0); - List combData = registers.stream() - .map(register -> { + List combData = registers.stream().map(register -> { + var builder2 = ClubArrayData.CombData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); - var builder2 = ClubArrayData.CombData.builder(); - AtomicInteger w = new AtomicInteger(0); - AtomicInteger l = new AtomicInteger(0); - AtomicInteger pointMake = new AtomicInteger(); - AtomicInteger pointTake = new AtomicInteger(); + matchModels.stream() + .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) + || register.getMembre().equals(m.getC2_id()))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((register.getMembre().equals(matchModel.getC1_id()) && win > 0) || + register.getMembre().equals(matchModel.getC2_id()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } - matchModels.stream() - .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) - || register.getMembre().equals(m.getC2_id()))) - .forEach(matchModel -> { - int win = matchModel.win(); - if ((register.getMembre() - .equals(matchModel.getC1_id()) && win > 0) || - register.getMembre() - .equals(matchModel.getC2_id()) && win < 0) { - w.getAndIncrement(); - } else { - l.getAndIncrement(); - } + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (register.getMembre().equals(matchModel.getC1_id())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) - .forEach(score -> { - if (register.getMembre() - .equals(matchModel.getC1_id())) { - pointMake.addAndGet(score.getS1()); - pointTake.addAndGet(score.getS2()); - } else { - pointMake.addAndGet(score.getS2()); - pointTake.addAndGet(score.getS1()); - } - }); - }); + Categorie categorie = register.getCategorie(); + if (categorie == null) + categorie = register.getMembre().getCategorie(); - Categorie categorie = register.getCategorie(); - if (categorie == null) - categorie = register.getMembre().getCategorie(); + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(register.getName()); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); - builder2.name(register.getName()); - builder2.w(w.get()); - builder2.l(l.get()); - builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); - builder2.pointMake(pointMake.get()); - builder2.pointTake(pointTake.get()); - builder2.ratioPoint( - (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + tt_win.addAndGet(w.get()); + tt_match.addAndGet(w.get() + l.get()); - tt_win.addAndGet(w.get()); - tt_match.addAndGet(w.get() + l.get()); + return builder2.build(); + }) + .sorted(Comparator.comparing(ClubArrayData.CombData::name)) + .toList(); - return builder2.build(); - }) - .sorted(Comparator.comparing(ClubArrayData.CombData::name)) - .toList(); + builder.nb_match(tt_match.get()); + builder.match_w(tt_win.get()); + builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); + builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); + builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); + builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); + builder.combs(combData); - builder.nb_match(tt_match.get()); - builder.match_w(tt_win.get()); - builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) - .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); - builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); - builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); - builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) - .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); - builder.combs(combData); - - return builder.build(); - }) - ); + return builder.build(); + }); } @Builder diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java new file mode 100644 index 0000000..c04c643 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java @@ -0,0 +1,23 @@ +package fr.titionfire.ffsaf.domain.service; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.HashMap; + +@ApplicationScoped +public class UpdateService { // For public result page + static HashMap lastUpdate = new HashMap<>(); + + public void setNewData(long id) { + lastUpdate.put(id, System.currentTimeMillis()); + } + + public boolean needUpdate(long id, long last_update) { + if (!lastUpdate.containsKey(id)) { + lastUpdate.put(id, System.currentTimeMillis() - 5000); + return true; + } + + return lastUpdate.getOrDefault(id, 0L) > last_update; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java new file mode 100644 index 0000000..aee3914 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java @@ -0,0 +1,85 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.ResultService; +import fr.titionfire.ffsaf.domain.service.UpdateService; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.HashMap; + +@Path("api/public/result/{id}") +public class ExternalResultEndpoints { + + @Inject + ResultService resultService; + + @Inject + UpdateService updateService; + + @PathParam("id") + private String id; + + @GET + @Path("/poule/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> list() { + return resultService.getCategoryList(id); + } + + @GET + @Path("/poule/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getArray(@QueryParam("poule") long poule, @DefaultValue("-1") @QueryParam("rf") long rf) { + if (poule == 0) + return Uni.createFrom().voidItem(); + + if (updateService.needUpdate(poule, rf)) { + return resultService.getCategoryPublic(id, poule); + } else { + return Uni.createFrom().voidItem(); + } + } + + @GET + @Path("/comb/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> combList() { + return resultService.getCombList(id); + } + + @GET + @Path("/comb/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getArray(@QueryParam("comb") String comb) { + if (comb.equals("0")) + return Uni.createFrom().item(""); + return resultService.getCombArrayPublic(id, comb.substring(0, comb.indexOf('¤')), comb.substring(comb.indexOf('¤') + 1)); + } + + @GET + @Path("/comb/get_all") + @Produces(MediaType.APPLICATION_JSON) + public Uni getAll() { + return resultService.getAllCombArrayPublic(id); + } + + + + @GET + @Path("/club/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> clubList() { + return resultService.getClubList(id); + } + + @GET + @Path("/club/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getClubArray(@QueryParam("club") long club) { + if (club == 0) + return Uni.createFrom().item(""); + return resultService.getClubArrayPublic(id, club); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java index 2054e52..3ff949f 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -21,6 +21,8 @@ public class ResultCategoryData { HashMap> matchs = new HashMap<>(); HashMap> rankArray = new HashMap<>(); ArrayList> trees; + String[] liceName; + long genTime; @Data @AllArgsConstructor diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index b541a2e..62e720b 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -1,5 +1,7 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; +import fr.titionfire.ffsaf.data.model.MembreModel; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; @@ -365,4 +367,16 @@ public class Utils { } return result.toString().trim(); } + + public static String getFullName(Object ...models) { + for (Object model : models){ + if (model == null) + continue; + if (model instanceof MembreModel membreModel) + return membreModel.getFname() + " " + membreModel.getLname(); + if (model instanceof CompetitionGuestModel guestModel) + return guestModel.getFname() + " " + guestModel.getLname(); + } + return ""; + } } diff --git a/src/main/webapp/.gitignore b/src/main/webapp/.gitignore index a547bf3..a461aa8 100644 --- a/src/main/webapp/.gitignore +++ b/src/main/webapp/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +/public/result_test.html diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js new file mode 100644 index 0000000..53ddd7b --- /dev/null +++ b/src/main/webapp/public/competition.js @@ -0,0 +1,946 @@ +let apiUrlRoot = ""; +const rootDiv = document.getElementById("safca_api_data"); + +const header = `

Résultat de la compétition :

` +const backButton = `Retour` +const cupImg = `` + +const voidFunction = () => { +} +let lastRf = 0; +let rfFonction = voidFunction; + +setInterval(() => { + rfFonction(); +}, 15000); + +function setSubPage(name) { + window.location.hash = name; + const location = name.split('/'); + console.log(location); + + switch (location[0]) { + case 'home': + homePage(); + break; + case 'poule': + poulePage(location); + break; + case 'comb': + combPage(location); + break; + case 'club': + clubPage(location); + break; + case 'all': + combsPage(); + break; + } +} + +function homePage() { + rootDiv.innerHTML = header; + + let content = document.createElement('div'); + content.innerHTML = ` + + ` + rootDiv.append(content) +} + +let loadingAnimationStep = 0; + +function startLoading(root) { + const id = "loading" + Math.random().toString(36); + let element = document.createElement('h2'); + element.id = id; + + const anim = setInterval(() => { + let str = "Chargement"; + for (let i = 0; i < loadingAnimationStep; i++) { + str += "."; + } + loadingAnimationStep = (loadingAnimationStep + 1) % 4; + element.innerText = str; + }, 500); + + root.append(element) + return {interval: anim, root: root, element: element}; +} + +function stopLoading(loading) { + clearInterval(loading['interval']); + loading['root'].removeChild(loading['element']); +} + +function scoreToString(score) { + const scorePrint = (s1) => { + switch (s1) { + case -997: + return "disc."; + case -998: + return "abs."; + case -999: + return "for."; + case -1000: + return ""; + default: + return String(s1); + } + } + + return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); +} + +function dateToString(date) { + if (date === null || date === undefined) + return ""; + + const date_ = new Date(date); + const date_2 = new Date(date); + const current = new Date(); + current.setHours(0, 0, 0, 0); + date_2.setHours(0, 0, 0, 0); + + let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24)); + if (d === 0) + return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else if (d === 1) + return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else if (d === 2) + return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else + return date_.toLocaleDateString(); +} + +function buildPouleMenu(isPoule, change_view) { + const menuDiv = document.createElement('div'); + menuDiv.id = 'menu'; + menuDiv.style.borderBottom = '1px solid #9EA0A1'; + menuDiv.style.paddingBottom = '25px'; + + const ul = document.createElement('ul'); + ul.id = 'onglets'; + ul.style.position = 'absolute'; + ul.style.border = '1px solid transparent'; + ul.style.padding = '0'; + ul.style.font = 'bold 11px Batang, arial, serif'; + ul.style.listStyleType = 'none'; + ul.style.left = '50%'; + ul.style.marginTop = '0'; + ul.style.width = '430px'; + ul.style.marginLeft = '-215px'; + + function createTab(text, isActive, onClickHandler) { + const li = document.createElement('li'); + if (isActive) { + li.className = 'active'; + li.style.borderBottom = '1px solid #fff'; + li.style.backgroundColor = '#fff'; + } else { + li.style.backgroundColor = '#F4F9FD'; + } + li.style.float = 'left'; + li.style.height = '21px'; + li.style.margin = '2px 2px 0 2px !important'; + li.style.margin = '1px 2px 0 2px'; + li.style.border = '1px solid #9EA0A1'; + + const a = document.createElement('a'); + a.href = 'javascript:void(0);'; + a.onclick = onClickHandler; + a.textContent = text; + a.style.display = 'block'; + a.style.color = '#666'; + a.style.textDecoration = 'none'; + a.style.padding = '4px'; + + a.addEventListener('mouseover', function () { + a.style.background = '#fff'; + }); + + a.addEventListener('mouseout', function () { + if (!isActive) + a.style.background = '#F4F9FD'; + }); + + li.appendChild(a); + return li; + } + + const li1 = createTab('Poule', isPoule, function () { + change_view(true); + }); + ul.appendChild(li1); + const li2 = createTab('Tournois', !isPoule, function () { + change_view(false); + }); + ul.appendChild(li2); + + menuDiv.appendChild(ul); + + return menuDiv; +} + +function buildMatchArray(matchs) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +
+ + + + + + + + + + ` + for (const match of matchs) { + arrayContent += ` + + + + + + + + ` + } + arrayContent += `
RougeScoresBleuDate
${match.red}${match.red_w ? cupImg : ""}${scoreToString(match.score)}${match.blue_w ? cupImg : ""}${match.blue}${dateToString((match.red_w || match.blue_w) ? match.date : null)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function buildRankArray(rankArray) { + const arrayDiv = document.createElement('div'); + let arrayContent = `
+ + + + + + + + + + + ` + for (const row of rankArray) { + arrayContent += ` + + + + + + + + ` + + } + arrayContent += `
PlaceNomVictoireRatioPoints marquésPoints reçus
${row.rank}${row.name}${row.win}${row.pointRate.toFixed(3)}${row.pointMake}${row.pointTake}
` + arrayDiv.innerHTML = arrayContent; + return arrayDiv; +} + +function buildTree(treeData) { + return drawGraph(initTree(treeData)) +} + +function poulePage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par poule

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + let currentPoule = 0; + const loadPoule = () => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/poule/data?poule=${currentPoule}&rf=${lastRf}`) + .then(response => { + if (response.status === 204) // No content => no update + return; + + response.json().then(poule => { + lastRf = (poule.genTime !== undefined) ? poule.genTime : 0; + // console.log(poule); + + if (location.length === 3 && poule.type < 3) { + location.pop(); + window.location.hash = location.join('/'); + } + + dataContainer.replaceChildren(); + + if (poule.type === 1) { + for (const g in poule.matchs) { + if (Object.keys(poule.matchs).length > 1) { + const text = document.createElement('h4'); + text.textContent = `Groupe ${g}`; + text.style.marginTop = '2em'; + dataContainer.append(text); + } + dataContainer.append(buildMatchArray(poule.matchs[g])); + dataContainer.append(buildRankArray(poule.rankArray[g])); + } + } else if (poule.type === 2) { + dataContainer.append(buildTree(poule['trees'])); + } else { + const change_view = (isPoule) => { + dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view)); + + if (isPoule) { + for (const g in poule.matchs) { + if (Object.keys(poule.matchs).length > 1) { + const text = document.createElement('h4'); + text.textContent = `Groupe ${g}`; + text.style.marginTop = '2em'; + dataContainer.append(text); + } + dataContainer.append(buildMatchArray(poule.matchs[g])); + dataContainer.append(buildRankArray(poule.rankArray[g])); + } + } else { + dataContainer.append(buildTree(poule['trees'])); + } + + location[2] = isPoule ? 1 : 2; + window.location.hash = location.join('/'); + } + change_view((location.length === 3) ? location[2] === '1' : true); + } + }) + }) + .catch(e => { + console.error(e); + dataContainer.replaceChildren(new Text("Erreur de chargement de la poule")); + }) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/poule/list`) + .then(response => response.json()) + .then(poule => { + const select = document.createElement('select'); + select.setAttribute('id', poule.id); + select.innerHTML = ``; + for (const pouleKey of Object.keys(poule).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + location[1] = Object.keys(poule).find(key => poule[key] === Number(e.target.value)); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + lastRf = 0; + currentPoule = e.target.value; + loadPoule(); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = poule[decodeURI(location[1])]; + select.value = tmp; + currentPoule = tmp + loadPoule(); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des poules"))) + .finally(() => stopLoading(loading)); + + rfFonction = () => { + if (currentPoule !== 0) + loadPoule(); + } + + rootDiv.append(content) +} + +function buildCombView(comb) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Info :

+
    +
  • Nom Prénom : ${comb.name}
  • +
  • Club : ${comb.club}
  • +
  • Catégorie : ${comb.cat}
  • +
+

Statistique :

+
    +
  • Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})
  • +
  • Points marqués : ${comb.pointMake}
  • +
  • Points reçus : ${comb.pointTake}
  • +
  • Ratio du score (point marqué / point reçu): ${comb.pointRatio.toFixed(3)}
  • +
+ +

Liste des matchs:

+ +
+ + + + + + + + + + ` + for (const match of comb.matchs) { + arrayContent += ` + + + + + + + + ` + } + arrayContent += `
DatePouleAdversaireScoresRatio
${dateToString(match.date)}${match.poule}${match.adv}${scoreToString(match.score)}${match.ratio.toFixed(3)}${match.win ? cupImg : ""}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function combPage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par combattant

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loadComb = (id) => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/data?comb=${id}`) + .then(response => response.json()) + .then(comb => { + console.log(comb); + dataContainer.replaceChildren(buildCombView(comb)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant"))) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/list`) + .then(response => response.json()) + .then(combs => { + const select = document.createElement('select'); + select.innerHTML = ``; + for (const comb of Object.keys(combs).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + location[1] = Object.keys(combs).find(key => combs[key] === e.target.value); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + loadComb(e.target.value); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = combs[decodeURI(location[1])]; + select.value = tmp; + loadComb(tmp); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des combattants"))) + .finally(() => stopLoading(loading)); + + rootDiv.append(content) +} + +function buildClubView(club) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Info :

+
    +
  • Nom : ${club.name}
  • +
  • Nombre d'inscris : ${club.nb_insc}
  • +
+

Statistique :

+
    +
  • Nombre de match disputé : ${club.nb_match}
  • +
  • Nombre de victoires : ${club.match_w}
  • +
  • Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}
  • +
  • Points marqués : ${club.pointMake}
  • +
  • Points reçus : ${club.pointTake}
  • +
  • Ratio de points moyen : ${club.ratioPoint.toFixed(3)}
  • +
+ +

Liste des menbres :

+ +
+ + + + + + + + + + + + ` + for (const comb of club.combs) { + arrayContent += ` + + + + + + + + + + ` + } + arrayContent += `
CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points
${comb.cat}${comb.name}${comb.w}${comb.l}${comb.ratioVictoire.toFixed(3)}${comb.pointMake}${comb.pointTake}${comb.ratioPoint.toFixed(3)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function clubPage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par club

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loadComb = (id) => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/club/data?club=${id}`) + .then(response => response.json()) + .then(club => { + console.log(club); + dataContainer.replaceChildren(buildClubView(club)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club"))) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/club/list`) + .then(response => response.json()) + .then(clubs => { + const select = document.createElement('select'); + select.innerHTML = ``; + for (const club of Object.keys(clubs).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + if (e.target.value === '0') + return; + location[1] = Object.keys(clubs).find(key => clubs[key] === Number(e.target.value)); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + loadComb(e.target.value); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = clubs[decodeURI(location[1])]; + select.value = tmp; + loadComb(tmp); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des clubs"))) + .finally(() => stopLoading(loading)); + + rootDiv.append(content) +} + +function buildCombsView(combs) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Statistique :

+
    +
  • Nombre d'inscris : ${combs.nb_insc}
  • +
  • Nombre de match disputé : ${combs.tt_match}
  • +
  • Points marqués : ${combs.point}
  • +
+ +

Liste des combattants :

+ +
+ + + + + + + + + + + + + ` + for (const comb of combs.combs) { + arrayContent += ` + + + + + + + + + + + ` + } + arrayContent += `
CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points
${comb.cat}${comb.club}${comb.name}${comb.w}${comb.l}${comb.ratioVictoire.toFixed(3)}${comb.pointMake}${comb.pointTake}${comb.ratioPoint.toFixed(3)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function combsPage() { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/get_all`) + .then(response => response.json()) + .then(combs => { + console.log(combs); + dataContainer.replaceChildren(buildCombsView(combs)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste"))) + .finally(() => stopLoading(loading)); + + content.append(dataContainer); + rootDiv.append(content) +} + +window.addEventListener("load", () => { + let path = document.getElementById('safca_api_script').src; + const urlParams = new URLSearchParams(new URL(path).search); + apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id"); + + console.log("apiUrlRoot:", apiUrlRoot) + + let hash = window.location.hash.substring(1); + if (hash.length === 0) + setSubPage('home'); + else + setSubPage(hash); +}); + +class TreeNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + } + + death() { + let dg = 0; + let dd = 0; + + if (this.right != null) + dg = this.right.death(); + + if (this.left != null) + dg = this.left.death(); + + return 1 + Math.max(dg, dd); + } +} + +function initTree(data_in) { + out = []; + for (const din of data_in) { + out.push(parseTree(din)); + } + + return out; +} + +function parseTree(data_in) { + if (data_in?.data == null) + return null; + + let node = new TreeNode(data_in.data); + node.left = parseTree(data_in?.left); + node.right = parseTree(data_in?.right); + + return node; +} + +const max_x = 500; +const size = 24; + +function getBounds(root) { + let px = max_x; + let py; + let maxx, minx, miny, maxy + + function drawNode(tree, px, py) { + let death = tree.death() - 1 + + if (death === 0) { + if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0; + if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0; + } else { + if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0)) + miny = py - size * 2 * death - ((size * 1.5 / 2) | 0); + if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8; + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + maxx = px; + minx = px; + miny = py - (size * 1.5 / 2) | 0; + maxy = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + px = px - size * 2 - size * 8; + if (minx > px) minx = px; + + drawNode(node, px, py); + //graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny); + py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = maxx; + } + } else { + minx = 0; + maxx = 0; + miny = 0; + maxy = 0; + } + + return [minx, maxx, miny, maxy]; +} + +function drawGraph(root = []) { + const canvas = document.createElement('canvas'); + canvas.id = "myCanvas"; + canvas.style.border = "1px solid grey"; + canvas.style.marginTop = "10px"; + + const ctx = canvas.getContext("2d"); + + const [minx, maxx, miny, maxy] = getBounds(root); + canvas.width = maxx - minx; + canvas.height = maxy - miny; + ctx.translate(-minx, -miny); + + + ctx.fillStyle = "#000000" + ctx.lineWidth = 2 + ctx.strokeStyle = "#000000" + + function printText(s, x, y, width, height, lineG, lineD) { + ctx.save(); + ctx.translate(x, y); + + let tSize = 17; + let ratioX = height * 1. / 20.; + + ctx.font = "100 " + tSize + "px Arial"; + + let mw = width - (ratioX * 2) | 0; + if (ctx.measureText(s).width > mw) { + let dTextSize = true; + do { + tSize--; + ctx.font = tSize + "px Arial"; + } while (ctx.measureText(s).width > mw && tSize > 10) + + if (!dTextSize || ctx.measureText(s).width > mw) { + let s = ""; + for (const string2 in s.split(" ")) { + if (ctx.measureText(s + string2).width >= mw) { + s += "..."; + break; + } else { + s += string2 + " " + } + } + } + } + + const text = ctx.measureText(s) + let dx = (width - text.width) / 2; + let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillText(s, dx, dy, width - dy); + ctx.restore(); + + ctx.beginPath(); + if (lineD) { + ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2); + ctx.lineTo(x + width, y + height / 2) + } + if (lineG) { + ctx.moveTo(x, y + height / 2); + ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2) + } + ctx.stroke(); + } + + function printScores(scores, px, py, scale) { + + ctx.save(); + ctx.translate(px - size * 2, py - size * scale); + ctx.font = "100 " + 14 + "px Arial"; + ctx.textBaseline = 'top'; + + for (let i = 0; i < scores.length; i++) { + const score = scores[i].s1+"-"+scores[i].s2; + const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; + const text = ctx.measureText(score); + let dx = (size * 2 - text.width) / 2; + let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillStyle = '#ffffffdd'; + ctx.fillRect(dx, size * 2 * scale / div * (i) + dy, text.width, 14); + ctx.fillStyle = "#000000"; + ctx.fillText(score, dx, size * 2 * scale / div * (i) + dy, size * 2); + } + ctx.restore(); + } + + function drawNode(tree, px, py) { + ctx.beginPath() + ctx.moveTo(px, py) + ctx.lineTo(px - size, py) + ctx.stroke(); + + let death = tree.death() - 1 + let match = tree.data + + if (death === 0) { + ctx.beginPath() + ctx.moveTo(px - size, py + size) + ctx.lineTo(px - size, py - size) + + ctx.moveTo(px - size, py + size) + ctx.lineTo(px - size * 2, py + size) + ctx.moveTo(px - size, py - size) + ctx.lineTo(px - size * 2, py - size) + ctx.stroke(); + + printScores(match.scores, px, py, 1); + + ctx.fillStyle = "#FF0000"; + printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, false, true) + + ctx.fillStyle = "#0000FF"; + printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, false, true) + + if (max_y < py + size + ((size * 1.5 / 2) | 0)) max_y = py + size + (size * 1.5 / 2) | 0; + } else { + ctx.beginPath() + ctx.moveTo(px - size, py) + ctx.lineTo(px - size, py + size * 2 * death) + ctx.moveTo(px - size, py) + ctx.lineTo(px - size, py - size * 2 * death) + + ctx.moveTo(px - size, py + size * 2 * death) + ctx.lineTo(px - size * 2, py + size * 2 * death) + ctx.moveTo(px - size, py - size * 2 * death) + ctx.lineTo(px - size * 2, py - size * 2 * death) + ctx.stroke(); + + printScores(match.scores, px, py, 1.5); + + ctx.fillStyle = "#FF0000"; + printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, true, true) + + ctx.fillStyle = "#0000FF"; + printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, true, true) + + if (max_y < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + max_y = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + + ctx.stroke(); + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + function win(scores) { + let sum = 0; + for (const score of scores) { + if (score.s1 === -1000 || score.s2 === -1000) + continue; + + if (score.s1 > score.s2) + sum++; + else if (score.s1 < score.s2) + sum--; + } + return sum; + } + + let px = max_x; + let py; + let max_y + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + max_y = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + let win_name = ""; + if (node.data.end) { + if (win(node.data.scores) > 0) + win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName; + else + win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName; + } + + + ctx.fillStyle = "#18A918"; + printText(win_name, px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0), + size * 8, (size * 1.5) | 0, true, false); + + px = px - size * 2 - size * 8; + + drawNode(node, px, py); + + py = max_y + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = max_x; + } + } + + return canvas; +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index a844107..8454f11 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -11,8 +11,11 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import JSZip from "jszip"; import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx"; +import {faGlobe} from "@fortawesome/free-solid-svg-icons"; -export function CMAdmin() { +const vite_url = import.meta.env.VITE_URL; + +export function CMAdmin({compUuid}) { const [catId, setCatId] = useState(null); const [cat, setCat] = useState(null); const menuActions = useRef({}); @@ -46,7 +49,7 @@ export function CMAdmin() {
- +
} @@ -155,7 +158,7 @@ async function downloadResourcesAsZip(resourceList) { progressText.textContent = "Téléchargement terminé !"; } -function Menu({menuActions}) { +function Menu({menuActions, compUuid}) { const e = document.getElementById("actionMenu") const longPress = useRef({time: null, timer: null, button: null}); const obsModal = useRef(null); @@ -213,6 +216,16 @@ function Menu({menuActions}) { exportOBSConfiguration(adresse, password, assets_dir) } + const copyScriptToClipboard = () => { + navigator.clipboard.writeText(`
+ ` + ).then(() => { + toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress."); + }).catch(err => { + toast.error("Erreur lors de la copie dans le presse-papier : " + err); + }); + } + if (!e) return <>; return <> @@ -220,11 +233,16 @@ function Menu({menuActions}) { <>
longPressDown("obs")} onMouseUp={() => longPressUp("obs")} data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/> + copyScriptToClipboard()} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Copier le scripte d'intégration"/> , document.getElementById("actionMenu"))}