feat: add classement match system

This commit is contained in:
2026-02-05 21:29:27 +01:00
parent e2197d0712
commit 89d9e04a6f
24 changed files with 678 additions and 127 deletions

View File

@@ -104,8 +104,13 @@ function scorePrint(s1) {
return String(s1);
}
}
function scoreToString(score) {
return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ");
if (score.length === 0)
return "";
if (score.at(0) instanceof Array)
return score.map(s => scorePrint(s.at(0)) + "-" + scorePrint(s.at(1))).join(" | ");
return score.map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | ");
}
function dateToString(date) {
@@ -129,7 +134,7 @@ function dateToString(date) {
return date_.toLocaleDateString();
}
function buildPouleMenu(isPoule, change_view) {
function buildPouleMenu(isPoule, change_view, isClassement = false) {
const menuDiv = document.createElement('div');
menuDiv.id = 'menu';
menuDiv.style.borderBottom = '1px solid #9EA0A1';
@@ -188,7 +193,7 @@ function buildPouleMenu(isPoule, change_view) {
change_view(true);
});
ul.appendChild(li1);
const li2 = createTab(i18next.t('tournois'), !isPoule, function () {
const li2 = createTab(isClassement ? i18next.t('classement') : i18next.t('tournois'), !isPoule, function () {
change_view(false);
});
ul.appendChild(li2);
@@ -262,7 +267,7 @@ function buildRankArray(rankArray) {
}
function buildTree(treeData) {
return drawGraph(initTree(treeData))
return drawGraph(initTree(treeData.filter(d => d.data.level >= 0)))
}
function poulePage(location) {
@@ -310,7 +315,7 @@ function poulePage(location) {
dataContainer.append(buildTree(poule['trees']));
} else {
const change_view = (isPoule) => {
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view));
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view, poule['treeIsClassement']));
if (isPoule) {
for (const g in poule.matchs) {
@@ -325,6 +330,19 @@ function poulePage(location) {
}
} else {
dataContainer.append(buildTree(poule['trees']));
if (poule['treeIsClassement'] && poule['trees'].some(d => d.data.level <= -10)) {
dataContainer.append(buildMatchArray(
poule['trees'].filter(d => d.data.level < 0).reverse().map(d => ({
red: d.data.c1FullName,
blue: d.data.c2FullName,
score: d.data.scores,
end: d.data.end,
red_w: d.data.win > 0,
blue_w: d.data.win < 0,
eq: d.data.win === 0,
date: d.data.date,
}))));
}
}
location[2] = isPoule ? 1 : 2;
@@ -392,10 +410,10 @@ function buildCombView(comb) {
<h3>${i18next.t('statistique')} :</h3>
<ul>
<li>${i18next.t('tauxDeVictoire2', {
nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.filter(m => m.end).length * 100).toFixed(0),
victoires: comb.totalWin,
matchs: comb.matchs.filter(m => m.end).length
})}
nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.filter(m => m.end).length * 100).toFixed(0),
victoires: comb.totalWin,
matchs: comb.matchs.filter(m => m.end).length
})}
</li>
<li>${i18next.t('pointsMarqués2', {nb: comb.pointMake})}</li>
<li>${i18next.t('pointsReçus2', {nb: comb.pointTake})}</li>

View File

@@ -35,6 +35,7 @@
"chrono.entrezLeTempsEnS": "Enter time in seconds",
"chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}",
"chronomètre": "Stopwatch",
"classement": "Ranking",
"club": "Club",
"compétition": "Competition",
"compétitionManager": "Competition manager",
@@ -55,6 +56,8 @@
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
"contre": "vs",
"couleur": "Color",
"créerLesMatchesDeClassement": "Create the ranking matches",
"créerLesMatchesDeClassement.msg": "Ranking matches have already been set up/played; recreating these matches will delete them all (you will therefore lose any results). Please note down any information you wish to keep.",
"créerLesMatchs": "Create matches",
"date": "Date",
"demi-finalesEtFinales": "Semi-finals and finals",
@@ -80,6 +83,8 @@
"genre.na": "NA",
"individuelle": "Individual",
"inscrit": "Registered",
"leTournoiServiraDePhaseFinaleAuxPoules": "The tournament will serve as the final phase for the group stage.",
"lesCombattantsEnDehors": "Fighters not participating in the tournament will have a ranking match.",
"listeDesCartons": "List of cards",
"manche": "Round",
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
@@ -134,6 +139,9 @@
"toast.deleteCategory.error": "Error while deleting the category",
"toast.deleteCategory.pending": "Deleting category...",
"toast.deleteCategory.success": "Category deleted!",
"toast.matchs.classement.create.error": "Error while creating ranking matches.",
"toast.matchs.classement.create.pending": "Creating ranking matches in progress...",
"toast.matchs.classement.create.success": "Ranking matches created successfully.",
"toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.",

View File

@@ -10,6 +10,7 @@
"bleu": "Blue",
"catégorie": "Category",
"chargement": "Loading",
"classement": "Ranking",
"club": "Club",
"combattant": "Fighter",
"combattants": "Fighters",
@@ -64,5 +65,6 @@
"tournois": "Tournaments",
"tousLesCombattants": "All fighters",
"victoire": "Win",
"victoires": "Wins"
"victoires": "Wins",
"classementFinal": "Final standings"
}

View File

@@ -35,6 +35,7 @@
"chrono.entrezLeTempsEnS": "Entrez le temps en s",
"chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}",
"chronomètre": "Chronomètre",
"classement": "Classement",
"club": "Club",
"compétition": "Compétition",
"compétitionManager": "Compétition manager",
@@ -55,6 +56,8 @@
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
"contre": "contre",
"couleur": "Couleur",
"créerLesMatchesDeClassement": "Créer les matches de classement",
"créerLesMatchesDeClassement.msg": "Des matches de classement ont déjà été configurer/jouer, la recréation de ces matches vont tous les supprimer (vous perdre donc les résultats s'il y en a). Mercie de noter de votre côté les informations que vous voulez conserver.",
"créerLesMatchs": "Créer les matchs",
"date": "Date",
"demi-finalesEtFinales": "Demi-finales et finales",
@@ -80,6 +83,8 @@
"genre.na": "NA",
"individuelle": "Individuelle",
"inscrit": "Inscrit",
"leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules",
"lesCombattantsEnDehors": "Les combattants en dehors du tournoi auront un match de classement",
"listeDesCartons": "Liste des cartons",
"manche": "Manche",
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
@@ -134,6 +139,9 @@
"toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie",
"toast.deleteCategory.pending": "Suppression de la catégorie...",
"toast.deleteCategory.success": "Catégorie supprimée !",
"toast.matchs.classement.create.error": "Erreur lors de la création des matchs de classement.",
"toast.matchs.classement.create.pending": "Création des matchs de classement en cours...",
"toast.matchs.classement.create.success": "Matchs de classement créés avec succès.",
"toast.matchs.create.error": "Erreur lors de la création des matchs.",
"toast.matchs.create.pending": "Création des matchs en cours...",
"toast.matchs.create.success": "Matchs créés avec succès.",

View File

@@ -10,6 +10,7 @@
"bleu": "Bleu",
"catégorie": "Catégorie",
"chargement": "Chargement",
"classement": "Classement",
"club": "Club",
"combattant": "Combattant",
"combattants": "Combattants",
@@ -64,5 +65,6 @@
"tournois": "Tournois",
"tousLesCombattants": "Tous les combattants",
"victoire": "Victoire",
"victoires": "Victoires"
"victoires": "Victoires",
"classementFinal": "Classement final"
}

View File

@@ -18,7 +18,7 @@ import {getToastMessage} from "../../../utils/Tools.js";
import {copyStyles} from "../../../utils/copyStyles.js";
import {StateWindow} from "./StateWindow.jsx";
import {CombName, useCombs} from "../../../hooks/useComb.jsx";
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
const vite_url = import.meta.env.VITE_URL;
@@ -36,7 +36,10 @@ export function CMAdmin({compUuid}) {
...cat_,
name: data.name,
liceName: data.liceName,
type: data.type
type: data.type,
treeAreClassement: data.treeAreClassement,
fullClassement: data.fullClassement,
preset: data.preset,
}))
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
@@ -465,7 +468,7 @@ function TeamCardModal() {
<td><GetCard type={card.type}/></td>
<td>{card.teamName}</td>
</> : <>
<td>{card.teamCard ? "|-> " + t('team'): t('individuelle')} </td>
<td>{card.teamCard ? "|-> " + t('team') : t('individuelle')} </td>
<td><GetCard type={card.type}/></td>
<td><CombName combId={card.comb}/></td>
</>}
@@ -569,7 +572,7 @@ function CategoryHeader({
</div>
<div className="col" style={{margin: "auto 0", textAlign: "center"}}>
{cat &&
<div>Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? t('tournois') : ""} |
<div>Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? (cat.treeAreClassement ? t('classement') : t('tournois')) : ""} |
Zone: {cat.liceName}</div>}
</div>
<div className="col-auto">
@@ -603,6 +606,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const [lice, setLice] = useState("1")
const [poule, setPoule] = useState(true)
const [tournoi, setTournoi] = useState(false)
const [classement, setClassement] = useState(true)
const [fullClassement, setFullClassement] = useState(false)
const [size, setSize] = useState(4)
const [loserMatch, setLoserMatch] = useState(1)
const {t} = useTranslation("cm");
@@ -614,14 +619,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
setLice(state.liceName || "1");
setPoule(((state.type || 1) & 1) !== 0);
setTournoi((state.type & 2) !== 0);
setClassement(state.treeAreClassement !== undefined && state.treeAreClassement !== false);
setFullClassement(state.fullClassement !== undefined && state.fullClassement !== false);
if (state?.trees && state.trees.length >= 1) {
const tree = state.trees[0];
const trees_ = state?.trees ? state.trees.filter(t => t.level > 0) : null;
if (trees_ && trees_.length >= 1) {
const tree = trees_[0];
setSize(tree.getMaxChildrenAtDepth(tree.death() - 1) * 2);
if (state.trees.length === 1) {
if (trees_.length === 1) {
setLoserMatch(0);
} else if (state.trees.length === 2) {
} else if (trees_.length === 2) {
setLoserMatch(1);
} else {
setLoserMatch(-1);
@@ -647,24 +655,27 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
return;
}
const trees_ = state?.trees ? state.trees.filter(t => t.level > 0) : null;
if (state?.id) {
const applyChanges = () => {
const newData = {
id: state.id,
name: name.trim(),
liceName: lice.trim(),
type: nType
type: nType,
treeAreClassement: classement,
fullClassement: fullClassement
}
let nbMatch = -1;
let oldSubTree = -1;
const oldTrees = state?.trees || [];
const oldTrees = trees_ || [];
if (oldTrees.length >= 1) {
const tree = state.trees[0];
const tree = trees_[0];
nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1);
if (state.trees.length === 1)
if (trees_.length === 1)
oldSubTree = 0
else if (state.trees.length === 2)
else if (trees_.length === 2)
oldSubTree = 1
}
@@ -738,15 +749,6 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
}
})
}
const data = {
name: name.trim(),
liceName: lice.trim(),
type: poule + (tournoi << 1),
size: size,
loserMatch: loserMatch
}
console.log("Submitting category data:", data);
}
return <form onSubmit={handleSubmit}>
@@ -782,8 +784,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
onChange={e => setTournoi(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckDefault2">{t('tournoi')}</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault" disabled={!tournoi} checked={classement}
onChange={e => setClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault">
{t('leTournoiServiraDePhaseFinaleAuxPoules')}
</label>
</div>
</div>
<div className="mb-3">
<label htmlFor="sizeInput1" className="form-label">Nombre de combattants</label>
<input type="number" className="form-control" id="sizeInput1" placeholder="4" name="size" disabled={!tournoi} value={size}
@@ -821,6 +832,14 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
</div>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!tournoi || !classement}
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault2">
{t('lesCombattantsEnDehors')}
</label>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>

View File

@@ -6,12 +6,13 @@ import {DrawGraph} from "../../result/DrawGraph.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
import {virtualScore, win_end} from "../../../utils/Tools.js";
import {getToastMessage, virtualScore, win_end} from "../../../utils/Tools.js";
import "./CMTMatchPanel.css"
import {useOBS} from "../../../hooks/useOBS.jsx";
import {useTranslation} from "react-i18next";
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {ScorePanel} from "./ScoreAndCardPanel.jsx";
import {toast} from "react-toastify";
function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@@ -70,7 +71,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
function CMTMatchPanel({catId, cat, menuActions}) {
const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS();
const [trees, setTrees] = useState([]);
const [trees, setTrees] = useState({raw: [], formatted: []});
const [matches, reducer] = useReducer(MarchReducer, []);
const combDispatch = useCombsDispatch();
const cardDispatch = useCardsDispatch();
@@ -89,7 +90,10 @@ function CMTMatchPanel({catId, cat, menuActions}) {
setLoading(1);
sendRequest('getFullCategory', catId)
.then((data) => {
setTrees(data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
setTrees({
raw: data.trees.sort((a, b) => a.level - b.level),
formatted: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
})
cardDispatch({type: 'SET_ALL', payload: data.cards});
@@ -105,7 +109,10 @@ function CMTMatchPanel({catId, cat, menuActions}) {
const treeListener = ({data}) => {
if (data.length < 1 || data[0].categorie !== catId)
return
setTrees(data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
setTrees({
raw: data.sort((a, b) => a.level - b.level),
formatted: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
})
let matches2 = [];
let combsToAdd = [];
@@ -147,6 +154,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1);
const {sendRequest} = useWS();
const {t} = useTranslation("cm");
useEffect(() => {
@@ -156,6 +164,10 @@ function ListMatch({cat, matches, trees, menuActions}) {
setType(cat.type);
}, [cat]);
const handleCreatClassement = () => {
toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm"))
}
if (!cat)
return <></>;
@@ -169,7 +181,7 @@ function ListMatch({cat, matches, trees, menuActions}) {
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>{t('tournois')}
onClick={_ => setType(2)}>{(cat.treeAreClassement ? t('classement') : t('tournois'))}
</div>
</li>
</ul>
@@ -181,12 +193,14 @@ function ListMatch({cat, matches, trees, menuActions}) {
</>}
{type === 2 && <>
<BuildTree treeData={trees} matches={matches} menuActions={menuActions}/>
{cat.treeAreClassement && !matches.some(m => m.categorie === cat.id && m.categorie_ord === -42 && (m.c1 !== undefined || m.c2 !== undefined)) ? <>
<button className="btn btn-primary" onClick={handleCreatClassement}>{t('créerLesMatchesDeClassement')}</button>
</> : <BuildTree treeData={trees} matches={matches} cat={cat} menuActions={menuActions}/>}
</>}
</div>
}
function MatchList({matches, cat, menuActions}) {
function MatchList({matches, cat, menuActions, classement = false, currentMatch = null, setCurrentMatch, getNext}) {
const [activeMatch, setActiveMatch] = useState(null)
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
const publicAffDispatch = usePubAffDispatch();
@@ -195,12 +209,17 @@ function MatchList({matches, cat, menuActions}) {
const {sendNotify, setState} = useWS();
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
const marches2 = classement
? matches.filter(m => m.categorie_ord === -42)
.map(m => ({...m, ...win_end(m, cards_v)}))
: matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => {
if (classement)
return true;
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
}
@@ -225,16 +244,26 @@ function MatchList({matches, cat, menuActions}) {
useEffect(() => {
if (match && match.poule !== lice)
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
if (!classement)
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
}, [lice]);
useEffect(() => {
if (marches2.length === 0)
return;
if (marches2.some(m => m.id === activeMatch))
if (marches2.some(m => m.id === (classement ? currentMatch?.matchSelect : activeMatch)))
return;
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
if (classement) {
getNext.current = (id) => {
if (id === null)
return marches2.findLast((m, index) => !m.end && isActiveMatch(index))?.id;
const index = marches2.findIndex(m => m.id === id);
return marches2.slice(0, index).reverse().find((m, index2) => !m.end && isActiveMatch(marches2.length - 1 - index2))?.id;
}
} else {
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
}
}, [matches])
useEffect(() => {
@@ -242,6 +271,13 @@ function MatchList({matches, cat, menuActions}) {
sendNotify("sendSelectMatch", activeMatch);
}, [activeMatch]);
const handleMatchClick = (matchId) => {
if (classement) {
setCurrentMatch({matchSelect: matchId, matchNext: marches2.reverse().find(m => !m.end && m.id !== matchId)?.id});
} else {
setActiveMatch(matchId);
}
}
const GetCard = ({combId, match, cat}) => {
const c = getHeightCardForCombInMatch(combId, match)
@@ -287,8 +323,8 @@ function MatchList({matches, cat, menuActions}) {
<table className="table table-striped table-hover">
<thead>
<tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>}
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>}
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
@@ -299,11 +335,11 @@ function MatchList({matches, cat, menuActions}) {
<tbody className="table-group-divider">
{marches2.map((m, index) => (
<tr key={m.id}
className={m.id === activeMatch ? "table-primary" : (m.end ? "table-success" : (isActiveMatch(index) ? "" : "table-warning"))}
onClick={() => setActiveMatch(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>
className={m.id === (classement ? currentMatch?.matchSelect : activeMatch) ? "table-primary" : (m.end ? "table-success" : (isActiveMatch(index) ? "" : "table-warning"))}
onClick={() => handleMatchClick(m.id)}>
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td>}
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>}
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{index >= firstIndex ? index + 1 - firstIndex : ""}</th>
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
@@ -320,18 +356,20 @@ function MatchList({matches, cat, menuActions}) {
</table>
</div>
{activeMatch &&
{activeMatch && !classement &&
<LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</>
}
function BuildTree({treeData, matches, menuActions}) {
function BuildTree({treeData, matches, cat, menuActions}) {
const scrollRef = useRef(null)
const [currentMatch, setCurrentMatch] = useState(null)
const {getComb} = useCombs()
const publicAffDispatch = usePubAffDispatch();
const {cards_v} = useCards();
const {sendNotify, setState} = useWS();
const getNext = useRef(null);
const rtrees = useRef(null);
const match = matches.find(m => m.id === currentMatch?.matchSelect)
useEffect(() => {
@@ -350,11 +388,11 @@ function BuildTree({treeData, matches, menuActions}) {
}
}, [next_match]);
function parseTree(data_in) {
function parseTree(data_in, matches_) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const matchData = matches_.find(m => m.id === data_in.data)
const c1 = getComb(matchData?.c1)
const c2 = getComb(matchData?.c2)
@@ -375,24 +413,57 @@ function BuildTree({treeData, matches, menuActions}) {
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
node.left = parseTree(data_in?.left, matches_)
node.right = parseTree(data_in?.right, matches_)
return node
}
function initTree(data_in) {
function initTree(data_in, matches_) {
let out = []
for (const din of data_in) {
out.push(parseTree(din))
let out2 = []
for (let i = 0; i < data_in.raw.length; i++) {
if (data_in.raw.at(i).level > -10) {
out.push(parseTree(data_in.formatted.at(i), matches_))
out2.push(parseTree(data_in.formatted.at(i), matches_))
}
}
return out
return [out, out2.reverse()]
}
const trees = initTree(treeData);
const [trees, rTrees] = initTree(treeData, matches);
rtrees.current = rTrees;
useEffect(() => {
if (matches.length === 0)
return;
if (matches.some(m => m.id === currentMatch?.matchSelect))
return;
const timeout = setTimeout(() => {
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
const matchId = ((getNext.current) ? getNext.current(null) : null) || new TreeNode(null).nextMatchTree(rTrees_);
const next = matchId ? ((getNext.current) ? getNext.current(matchId) : null) || new TreeNode(matchId).nextMatchTree(rTrees_) : null;
setCurrentMatch({matchSelect: matchId, matchNext: next});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}, 200);
return () => clearTimeout(timeout);
}, [matches])
const setCurrentMatch_ = (o) => {
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
const matchId = o.matchSelect ? o.matchSelect : new TreeNode(null).nextMatchTree(rTrees_);
const next = o.matchNext ? o.matchNext : new TreeNode(matchId).nextMatchTree(rTrees_);
setCurrentMatch({matchSelect: matchId, matchNext: next});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}
const onMatchClick = (rect, matchId, __) => {
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(rTrees_)});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}
@@ -406,6 +477,10 @@ function BuildTree({treeData, matches, menuActions}) {
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23} cards={cards_v}/>
{cat.fullClassement &&
<MatchList matches={treeData.raw.filter(n => n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))}
cat={cat} menuActions={menuActions} classement={true} currentMatch={currentMatch} setCurrentMatch={setCurrentMatch_}
getNext={getNext}/>}
</div>
</div>

View File

@@ -251,7 +251,7 @@ function Menu({menuActions}) {
<button ref={teamCardModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#TeamCardModal"
style={{display: 'none'}}>
Launch OBS Modal
</button>s
</button>
<div className="modal fade" id="TeamCardModal" tabIndex="-1" aria-labelledby="TeamCardModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">

View File

@@ -20,6 +20,7 @@ import {useTranslation} from "react-i18next";
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {ScorePanel} from "./ScoreAndCardPanel.jsx";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
const vite_url = import.meta.env.VITE_URL;
@@ -62,7 +63,8 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
return
setCat(cat_ => ({
...cat_,
trees: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
trees: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
raw_trees: data.sort((a, b) => a.level - b.level)
}))
let matches2 = [];
@@ -122,7 +124,11 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
name: data.name,
liceName: data.liceName,
type: data.type,
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
raw_trees: data.trees.sort((a, b) => a.level - b.level),
treeAreClassement: data.treeAreClassement,
fullClassement: data.fullClassement,
preset: data.preset,
})
cardDispatch({type: 'SET_ALL', payload: data.cards});
@@ -360,6 +366,7 @@ function ListMatch({cat, matches, groups, reducer}) {
const {sendRequest} = useWS();
const [type, setType] = useState(1);
const bthRef = useRef(null);
const bthRef2 = useRef(null);
const {t} = useTranslation("cm");
useEffect(() => {
@@ -375,6 +382,17 @@ function ListMatch({cat, matches, groups, reducer}) {
recalculateMatch(0);
}
const handleCreatClassement_ = () => {
toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm"))
}
const handleCreatClassement = () => {
if (matches.some(m => m.categorie === cat.id && m.categorie_ord === -42 && m.c1 !== undefined && m.c2 !== undefined)) {
bthRef2.current.click();
return;
}
handleCreatClassement_()
}
const recalculateMatch = (mode) => {
let matchesToKeep = [];
let matchesToRemove = [];
@@ -416,7 +434,7 @@ function ListMatch({cat, matches, groups, reducer}) {
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>{t('tournois')}
onClick={_ => setType(2)}>{cat.treeAreClassement ? t('classement') : t('tournois')}
</div>
</li>
</ul>
@@ -428,7 +446,9 @@ function ListMatch({cat, matches, groups, reducer}) {
</>}
{type === 2 && <>
<BuildTree treeData={cat.trees} matches={matches} groups={groups}/>
<BuildTree treeData={cat.trees} treeRaw={cat.raw_trees} cat={cat} matches={matches} groups={groups}/>
{cat.treeAreClassement &&
<button className="btn btn-primary float-end" onClick={handleCreatClassement}>{t('créerLesMatchesDeClassement')}</button>}
</>}
<button ref={bthRef} data-bs-toggle="modal" data-bs-target="#makeMatchMode" style={{display: "none"}}>open</button>
@@ -457,10 +477,16 @@ function ListMatch({cat, matches, groups, reducer}) {
</div>
</div>
</div>
<button ref={bthRef2} data-bs-toggle="modal" data-bs-target="#confirm-regenerate-classement" style={{display: "none"}}>open</button>
<ConfirmDialog id="confirm-regenerate-classement"
title={t('créerLesMatchesDeClassement')}
message={t('créerLesMatchesDeClassement.msg')}
onConfirm={handleCreatClassement_}/>
</>
}
function MatchList({matches, cat, groups, reducer}) {
function MatchList({matches, cat, groups, reducer, classement = false}) {
const {sendRequest} = useWS();
const setLoading = useLoadingSwitcher()
const selectRef = useRef(null)
@@ -475,9 +501,13 @@ function MatchList({matches, cat, groups, reducer}) {
const matchModal = useRef(null);
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
const marches2 = classement
? matches.filter(m => m.categorie_ord === -42)
.map(m => ({...m, ...win_end(m, cards_v)}))
: matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
marches2.forEach(m => {
if (m.end && (!m.scores || m.scores.length === 0))
m.scores = [{n_round: 0, s1: 0, s2: 0}];
@@ -491,6 +521,9 @@ function MatchList({matches, cat, groups, reducer}) {
);
const handleDragEnd = async (event) => {
if (classement)
return;
const {active, over} = event;
if (active.id !== over.id) {
const newIndex = marches2.findIndex(m => m.id === over.id);
@@ -570,7 +603,7 @@ function MatchList({matches, cat, groups, reducer}) {
const handleDelMatch = (matchId) => {
const match = matches.find(m => m.id === matchId)
if (!match)
if (!match || classement)
return;
if (!confirm(t('confirm1')))
@@ -585,7 +618,7 @@ function MatchList({matches, cat, groups, reducer}) {
const match = matches.find(m => m.id === matchId)
if (!match)
return;
setModalMatchId (matchId);
setModalMatchId(matchId);
matchModal.current.click();
}
@@ -645,41 +678,46 @@ function MatchList({matches, cat, groups, reducer}) {
<thead>
<tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('poule')}</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('zone')}</th>
{!classement &&
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('poule')}</th>}
{!classement &&
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('zone')}</th>}
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">{t('résultat')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col"></th>
{!classement && <th style={{textAlign: "center"}} scope="col"></th>}
{!classement && <th style={{textAlign: "center"}} scope="col"></th>}
</tr>
</thead>
<tbody className="table-group-divider">
{marches2.map((m, index) => (
<SortableRow key={m.id} id={m.id}>
<th style={{textAlign: "center", cursor: "auto"}} scope="row">{index + 1}</th>
<td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>
<td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</td>
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>}
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</td>}
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}
onClick={e => handleCombClick(e, m.id, m.c1)}>
<small className="position-relative"><CombName combId={m.c1}/>
<GetCard match={m} combId={m.c1} cat={cat}/></small></td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}
onClick={e => handleCombClick(e, m.id, m.c2)}>
<small className="position-relative"><CombName combId={m.c2}/>
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
<td style={{textAlign: "center", cursor: "auto"}}>{scoreToString2(m, cards_v)}</td>
<td style={{textAlign: "center", cursor: "pointer", color: "#1381ff"}} onClick={_ => handleEditMatch(m.id)}>
<FontAwesomeIcon icon={faPen}/></td>
<td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => handleDelMatch(m.id)}>
<FontAwesomeIcon icon={faTrash}/></td>
<td style={{textAlign: "center", cursor: "grab"}}></td>
{!classement &&
<td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => handleDelMatch(m.id)}>
<FontAwesomeIcon icon={faTrash}/></td>}
<td style={{textAlign: "center", cursor: "grab"}} hidden={classement}></td>
</SortableRow>
))}
<tr>
{!classement && <tr>
<td>-</td>
<td></td>
<td></td>
@@ -695,7 +733,7 @@ function MatchList({matches, cat, groups, reducer}) {
<td></td>
<td></td>
<td></td>
</tr>
</tr>}
</tbody>
</table>
</div>
@@ -770,7 +808,7 @@ function SortableRow({id, children}) {
);
}
function BuildTree({treeData, matches, groups}) {
function BuildTree({treeData, treeRaw, matches, cat, groups}) {
const scrollRef = useRef(null)
const selectRef = useRef(null)
const lastMatchClick = useRef(null)
@@ -811,10 +849,12 @@ function BuildTree({treeData, matches, groups}) {
return node
}
function initTree(data_in) {
function initTree(data_in, data_raw) {
let out = []
for (const din of data_in) {
out.push(parseTree(din))
for (let i = 0; i < data_raw.length; i++) {
if (data_raw.at(i).level > -10) {
out.push(parseTree(data_in.at(i)))
}
}
return out
}
@@ -878,7 +918,10 @@ function BuildTree({treeData, matches, groups}) {
const combsIDs = groups.map(m => m.id);
return <div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={initTree(treeData)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid} cards={cards_v}/>
<DrawGraph root={initTree(treeData, treeRaw)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid} cards={cards_v}/>
{cat.fullClassement &&
<MatchList matches={treeRaw.filter(n => n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))}
groups={groups} cat={cat} reducer={undefined} classement={true}/>}
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
<option value={0}>{t('--SélectionnerUnCombattant--')}</option>

View File

@@ -14,6 +14,7 @@ function CupImg() {
style={{width: "16px"}} src="/img/171891.png"
alt=""/>
}
function CupImg2() {
return <img decoding="async" loading="lazy" width="16" height="16" className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png"
@@ -146,13 +147,52 @@ function BuildTree({treeData}) {
function initTree(data_in) {
let out = [];
for (const din of data_in) {
for (const din of data_in.filter(d => d.data.level >= 0)) {
out.push(parseTree(din));
}
return out;
}
return <DrawGraph root={initTree(treeData)}/>
return <>
<DrawGraph root={initTree(treeData)}/>
{treeData.some(d => d.data.level <= -10) &&
<BuildMatchArray matchs={treeData.filter(d => d.data.level <= -10).reverse().map(d => ({
red: d.data.c1FullName,
blue: d.data.c2FullName,
score: d.data.scores,
red_w: d.data.win > 0,
blue_w: d.data.win < 0,
eq: d.data.win === 0
}))}/>}
</>
}
function ClassementList({classement}) {
const {t} = useTranslation('result');
const classement2 = classement.sort((a, b) => {
if (a.rank === b.rank)
return a.name.localeCompare(b.name);
return a.rank - b.rank;
})
return <>
<h3 style={{marginTop: "2em"}}>{t('classementFinal')}</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>Place</th>
<th scope="col" style={{textAlign: "center"}}>Nom</th>
</tr>
</thead>
<tbody>
{classement2.map((row, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{row.rank}</td>
<td style={{textAlign: "left"}}>{row.name}</td>
</tr>)}
</tbody>
</table>
</>
}
function CategoryList({uuid}) {
@@ -212,7 +252,7 @@ function CategoryResult({uuid, catId}) {
</li>
<li className="nav-item">
<a className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} href="#"
onClick={_ => setType(2)}>{t('tournois')}</a>
onClick={_ => setType(2)}>{data.treeIsClassement ? t('classement') : t('tournois')}</a>
</li>
</ul>
</>}
@@ -227,6 +267,7 @@ function CategoryResult({uuid, catId}) {
{type === 2 && <>
<BuildTree treeData={data.trees}/>
{data.treeIsClassement && <ClassementList classement={data.classement}/>}
</>}
</>

View File

@@ -22,8 +22,18 @@ export function isClubAdmin(userinfo) {
export const errFormater = (data, msg) => {
if (!data)
return msg
if (!data.response)
return `${msg} (😕 ${data.message})`
if (!data.response) {
if (data.message)
return `${msg} (${data.message} 😕)`
if (typeof data === 'string' || data instanceof String) {
if (/.*D[a-zA-Z]+Exception:.*/.test(data))
return `${msg} (${data.split(":").slice(1).join(":").trim()} 😕)`
else
return `${msg}`
}
return `${msg} (${data} 😕)`
}
if (typeof data.response.data === 'string' || data.response.data instanceof String)
return `${msg} (${data.response.statusText}: ${data.response.data}) 😕`
return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕`