dev-comp #72
510
src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx
Normal file
510
src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
import React, {useEffect, useRef, useState, useReducer} from "react";
|
||||||
|
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||||
|
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||||
|
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
|
||||||
|
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 {scorePrint, win} from "../../../utils/Tools.js";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
|
function CupImg() {
|
||||||
|
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||||
|
style={{width: "16px"}} src="/img/171891.png"
|
||||||
|
alt=""/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategorieSelect({catId, setCatId}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
||||||
|
const {dispatch} = useWS();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const categoryListener = ({data}) => {
|
||||||
|
setCats([...cats.filter(c => c.id !== data.id), data])
|
||||||
|
}
|
||||||
|
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||||
|
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
||||||
|
}, [cats]);
|
||||||
|
|
||||||
|
const cat = cats?.find(c => c.id === catId);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="input-group">
|
||||||
|
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
|
||||||
|
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
|
||||||
|
{cats && <option value={-1}></option>}
|
||||||
|
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.name}</option>))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{catId !== -1 && <CMTMatchPanel catId={catId} cat={cat}/>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function CMTMatchPanel({catId, cat}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {sendRequest, dispatch} = useWS();
|
||||||
|
const [trees, setTrees] = useState([]);
|
||||||
|
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||||
|
const combDispatch = useCombsDispatch();
|
||||||
|
|
||||||
|
function readAndConvertMatch(matches, data, combsToAdd) {
|
||||||
|
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
|
||||||
|
if (data.c1)
|
||||||
|
combsToAdd.push(data.c1)
|
||||||
|
if (data.c2)
|
||||||
|
combsToAdd.push(data.c2)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!catId)
|
||||||
|
return;
|
||||||
|
setLoading(1);
|
||||||
|
sendRequest('getFullCategory', catId)
|
||||||
|
.then((data) => {
|
||||||
|
setTrees(data.trees.map(d => from_sendTree(d, true)))
|
||||||
|
|
||||||
|
let matches2 = [];
|
||||||
|
let combsToAdd = [];
|
||||||
|
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||||
|
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||||
|
|
||||||
|
reducer({type: 'REPLACE_ALL', payload: matches2});
|
||||||
|
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
|
||||||
|
}).finally(() => setLoading(0))
|
||||||
|
|
||||||
|
const treeListener = ({data}) => {
|
||||||
|
if (data.length < 1 || data[0].categorie !== catId)
|
||||||
|
return
|
||||||
|
setTrees(data.map(d => from_sendTree(d, true)))
|
||||||
|
|
||||||
|
let matches2 = [];
|
||||||
|
let combsToAdd = [];
|
||||||
|
data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||||
|
reducer({type: 'REPLACE_TREE', payload: matches2});
|
||||||
|
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchListener = ({data: datas}) => {
|
||||||
|
for (const data of datas) {
|
||||||
|
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}})
|
||||||
|
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchOrder = ({data}) => {
|
||||||
|
reducer({type: 'REORDER', payload: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMatch = ({data: datas}) => {
|
||||||
|
for (const data of datas)
|
||||||
|
reducer({type: 'REMOVE', payload: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
|
||||||
|
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
|
||||||
|
dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}})
|
||||||
|
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
|
||||||
|
return () => {
|
||||||
|
dispatch({type: 'removeListener', payload: treeListener})
|
||||||
|
dispatch({type: 'removeListener', payload: matchListener})
|
||||||
|
dispatch({type: 'removeListener', payload: matchOrder})
|
||||||
|
dispatch({type: 'removeListener', payload: deleteMatch})
|
||||||
|
}
|
||||||
|
}, [catId]);
|
||||||
|
|
||||||
|
return <ListMatch cat={cat} matches={matches} trees={trees}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListMatch({cat, matches, trees}) {
|
||||||
|
const [type, setType] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((cat.type & type) === 0)
|
||||||
|
setType(cat.type);
|
||||||
|
}, [cat]);
|
||||||
|
|
||||||
|
return <div style={{marginTop: "1em"}}>
|
||||||
|
{cat && cat.type === 3 && <>
|
||||||
|
<ul className="nav nav-tabs">
|
||||||
|
<li className="nav-item">
|
||||||
|
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
|
||||||
|
onClick={_ => setType(1)}>Poule
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
|
||||||
|
onClick={_ => setType(2)}>Tournois
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
{type === 1 && <>
|
||||||
|
<MatchList matches={matches} cat={cat}/>
|
||||||
|
</>}
|
||||||
|
|
||||||
|
{type === 2 && <>
|
||||||
|
<BuildTree treeData={trees} matches={matches}/>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchList({matches, cat}) {
|
||||||
|
const [activeMatch, setActiveMatch] = useState(null)
|
||||||
|
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "A")
|
||||||
|
const publicAffDispatch = usePubAffDispatch();
|
||||||
|
|
||||||
|
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: win(m.scores)}))
|
||||||
|
|
||||||
|
const match = matches.find(m => m.id === activeMatch)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!match) {
|
||||||
|
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
|
||||||
|
} else {
|
||||||
|
publicAffDispatch({
|
||||||
|
type: 'SET_DATA',
|
||||||
|
payload: {
|
||||||
|
c1: match.c1,
|
||||||
|
c2: match.c2,
|
||||||
|
next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [match]);
|
||||||
|
//useEffect(() => {
|
||||||
|
// if (activeMatch !== null)
|
||||||
|
// setActiveMatch(null);
|
||||||
|
//}, [cat])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (match && match.poule !== lice)
|
||||||
|
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id)
|
||||||
|
}, [lice]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (marches2.length === 0)
|
||||||
|
return;
|
||||||
|
if (marches2.some(m => m.id === activeMatch))
|
||||||
|
return;
|
||||||
|
|
||||||
|
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id);
|
||||||
|
}, [matches])
|
||||||
|
|
||||||
|
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
||||||
|
return <>
|
||||||
|
{liceName.length > 1 &&
|
||||||
|
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
|
||||||
|
<label className="input-group-text" htmlFor="selectLice">Lice</label>
|
||||||
|
<select className="form-select" id="selectLice" value={lice} onChange={e => {
|
||||||
|
setLice(e.target.value);
|
||||||
|
localStorage.setItem("cm_lice", e.target.value);
|
||||||
|
}}>
|
||||||
|
{liceName.map((l, index) => (
|
||||||
|
<option key={index} value={l}>{l}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className="table-responsive-xxl">
|
||||||
|
<table className="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th>
|
||||||
|
<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">N°</th>
|
||||||
|
<th style={{textAlign: "center"}} scope="col"></th>
|
||||||
|
<th style={{textAlign: "center"}} scope="col">Rouge</th>
|
||||||
|
<th style={{textAlign: "center"}} scope="col">Blue</th>
|
||||||
|
<th style={{textAlign: "center"}} scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="table-group-divider">
|
||||||
|
{marches2.map((m, index) => (
|
||||||
|
<tr key={m.id} className={m.id === activeMatch ? "table-info" : (m.poule === lice ? "" : "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>
|
||||||
|
<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>
|
||||||
|
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
|
||||||
|
<small><CombName combId={m.c1}/></small></td>
|
||||||
|
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
|
||||||
|
<small><CombName combId={m.c2}/></small></td>
|
||||||
|
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match}/></LoadingProvider>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function BuildTree({treeData, matches}) {
|
||||||
|
const scrollRef = useRef(null)
|
||||||
|
const [currentMatch, setCurrentMatch] = useState(null)
|
||||||
|
const {getComb} = useCombs()
|
||||||
|
const publicAffDispatch = usePubAffDispatch();
|
||||||
|
|
||||||
|
const match = matches.find(m => m.id === currentMatch?.matchSelect)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!match) {
|
||||||
|
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined}});
|
||||||
|
} else {
|
||||||
|
publicAffDispatch({type: 'SET_DATA', payload: {c1: match.c1, c2: match.c2}});
|
||||||
|
}
|
||||||
|
}, [match]);
|
||||||
|
const next_match = matches.find(m => m.id === currentMatch?.matchNext)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!next_match) {
|
||||||
|
publicAffDispatch({type: 'SET_DATA', payload: {next: []}});
|
||||||
|
} else {
|
||||||
|
publicAffDispatch({type: 'SET_DATA', payload: {next: [{c1: next_match.c1, c2: next_match.c2}]}});
|
||||||
|
}
|
||||||
|
}, [next_match]);
|
||||||
|
|
||||||
|
function parseTree(data_in) {
|
||||||
|
if (data_in?.data == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const matchData = matches.find(m => m.id === data_in.data)
|
||||||
|
const c1 = getComb(matchData?.c1)
|
||||||
|
const c2 = getComb(matchData?.c2)
|
||||||
|
|
||||||
|
|
||||||
|
let node = new TreeNode({
|
||||||
|
...matchData,
|
||||||
|
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)
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTree(data_in) {
|
||||||
|
let out = []
|
||||||
|
for (const din of data_in) {
|
||||||
|
out.push(parseTree(din))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const trees = initTree(treeData);
|
||||||
|
|
||||||
|
const onMatchClick = (rect, matchId, __) => {
|
||||||
|
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickVoid = () => {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<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}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentMatch?.matchSelect && <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match}/></LoadingProvider>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScorePanel({matchId, match}) {
|
||||||
|
const {sendRequest} = useWS()
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
|
||||||
|
const [end, setEnd] = useState(match?.end || false)
|
||||||
|
const [scoreIn, setScoreIn] = useState("")
|
||||||
|
const inputRef = useRef(null)
|
||||||
|
const tableRef = useRef(null)
|
||||||
|
const scoreRef = useRef([])
|
||||||
|
const lastScoreClick = useRef(null)
|
||||||
|
|
||||||
|
const handleScoreClick = (e, round, comb) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const tableRect = tableRef.current.getBoundingClientRect();
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
|
||||||
|
updateScore();
|
||||||
|
|
||||||
|
const sel = inputRef.current;
|
||||||
|
sel.style.top = (rect.y - tableRect.y) + "px";
|
||||||
|
sel.style.left = (rect.x - tableRect.x) + "px";
|
||||||
|
sel.style.width = rect.width + "px";
|
||||||
|
sel.style.height = rect.height + "px";
|
||||||
|
sel.style.display = "block";
|
||||||
|
|
||||||
|
if (round === -1) {
|
||||||
|
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
||||||
|
setScoreIn("");
|
||||||
|
console.log("Setting for new round", maxRound);
|
||||||
|
lastScoreClick.current = {matchId: matchId, round: maxRound, comb};
|
||||||
|
} else {
|
||||||
|
const score = match.scores.find(s => s.n_round === round);
|
||||||
|
setScoreIn((comb === 1 ? score?.s1 : score?.s2) || "");
|
||||||
|
lastScoreClick.current = {matchId: matchId, round, comb};
|
||||||
|
setTimeout(() => inputRef.current.select(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateScore = () => {
|
||||||
|
if (lastScoreClick.current !== null) {
|
||||||
|
const {matchId, round, comb} = lastScoreClick.current;
|
||||||
|
lastScoreClick.current = null;
|
||||||
|
|
||||||
|
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn);
|
||||||
|
|
||||||
|
const score = match.scores.find(s => s.n_round === round);
|
||||||
|
let newScore;
|
||||||
|
if (score) {
|
||||||
|
if (comb === 1)
|
||||||
|
newScore = {...score, s1: scoreIn_};
|
||||||
|
else
|
||||||
|
newScore = {...score, s2: scoreIn_};
|
||||||
|
|
||||||
|
if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)};
|
||||||
|
if (newScore.s1 === -1000 && newScore.s2 === -1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Updating score", matchId, newScore);
|
||||||
|
|
||||||
|
setLoading(1)
|
||||||
|
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickVoid = () => {
|
||||||
|
updateScore();
|
||||||
|
|
||||||
|
const sel = inputRef.current;
|
||||||
|
sel.style.display = "none";
|
||||||
|
lastScoreClick.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!match || match?.end === end)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (end) {
|
||||||
|
if (win(match?.scores) === 0 && match.categorie_ord === -42) {
|
||||||
|
toast.error("Impossible de terminer un match nul en tournois.");
|
||||||
|
setEnd(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(1)
|
||||||
|
sendRequest('updateMatchEnd', {matchId: matchId, end})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(0)
|
||||||
|
})
|
||||||
|
}, [end]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onClickVoid()
|
||||||
|
}, [matchId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (match?.scores)
|
||||||
|
scoreRef.current = scoreRef.current.slice(0, match.scores.length);
|
||||||
|
}, [match?.scores]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!match)
|
||||||
|
return;
|
||||||
|
setEnd(match.end);
|
||||||
|
}, [match]);
|
||||||
|
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
|
const o = [...tooltipTriggerList]
|
||||||
|
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||||
|
|
||||||
|
const tt = "Score speciaux : <br/>" +
|
||||||
|
"-997 : disqualifié <br/>" +
|
||||||
|
"-998 : absent <br/>" +
|
||||||
|
"-999 : forfait"
|
||||||
|
|
||||||
|
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
|
||||||
|
return <div className="row" onClick={onClickVoid}>
|
||||||
|
<div ref={tableRef} className="col" style={{position: "relative"}}>
|
||||||
|
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
|
||||||
|
data-bs-html="true"/></h6>
|
||||||
|
<table className="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{textAlign: "center"}} scope="col">Manche</th>
|
||||||
|
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th>
|
||||||
|
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="table-group-divider">
|
||||||
|
{match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => (
|
||||||
|
<tr key={score.n_round}>
|
||||||
|
<th style={{textAlign: "center"}}>{score.n_round + 1}</th>
|
||||||
|
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2] = e}
|
||||||
|
onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)}</td>
|
||||||
|
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2 + 1] = e}
|
||||||
|
onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
<tr>
|
||||||
|
<th style={{textAlign: "center"}}></th>
|
||||||
|
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>-</td>
|
||||||
|
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>-
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div style={{textAlign: "right"}}>
|
||||||
|
<div className="form-check" style={{display: "inline-block"}}>
|
||||||
|
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
|
||||||
|
onChange={e => setEnd(e.target.checked)}/>
|
||||||
|
<label className="form-check-label" htmlFor="checkboxEnd">Terminé</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
|
||||||
|
max="999"
|
||||||
|
value={scoreIn} onChange={e => setScoreIn(e.target.value)}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
if (lastScoreClick.current !== null) {
|
||||||
|
const {round, comb} = lastScoreClick.current;
|
||||||
|
const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1);
|
||||||
|
if (nextIndex >= 0 && nextIndex < scoreRef.current.length) {
|
||||||
|
e.preventDefault();
|
||||||
|
scoreRef.current[nextIndex].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClickVoid();
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -1,14 +1,7 @@
|
|||||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
import {useRequestWS} from "../../../hooks/useWS.jsx";
|
||||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
import {useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||||
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
|
|
||||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
|
||||||
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import {DrawGraph} from "../../result/DrawGraph.jsx";
|
|
||||||
import {scorePrint, win} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {createPortal} from "react-dom";
|
import {createPortal} from "react-dom";
|
||||||
import {copyStyles} from "../../../utils/copyStyles.js";
|
import {copyStyles} from "../../../utils/copyStyles.js";
|
||||||
import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||||
@ -16,12 +9,7 @@ import {faDisplay} from "@fortawesome/free-solid-svg-icons";
|
|||||||
import {PubAffWindow} from "./PubAffWindow.jsx";
|
import {PubAffWindow} from "./PubAffWindow.jsx";
|
||||||
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
|
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
|
||||||
import {ChronoPanel} from "./CMTChronoPanel.jsx";
|
import {ChronoPanel} from "./CMTChronoPanel.jsx";
|
||||||
|
import {CategorieSelect} from "./CMTMatchPanel.jsx";
|
||||||
function CupImg() {
|
|
||||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
|
||||||
style={{width: "16px"}} src="/img/171891.png"
|
|
||||||
alt=""/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CMTable() {
|
export function CMTable() {
|
||||||
const combDispatch = useCombsDispatch()
|
const combDispatch = useCombsDispatch()
|
||||||
@ -128,496 +116,3 @@ function Menu() {
|
|||||||
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategorieSelect({catId, setCatId}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
|
||||||
const {dispatch} = useWS();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const categoryListener = ({data}) => {
|
|
||||||
setCats([...cats.filter(c => c.id !== data.id), data])
|
|
||||||
}
|
|
||||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
|
||||||
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
|
||||||
}, [cats]);
|
|
||||||
|
|
||||||
const cat = cats?.find(c => c.id === catId);
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="input-group">
|
|
||||||
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
|
|
||||||
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
|
|
||||||
{cats && <option value={-1}></option>}
|
|
||||||
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
|
|
||||||
<option key={c.id} value={c.id}>{c.name}</option>))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{catId !== -1 && <MatchPanel catId={catId} cat={cat}/>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchPanel({catId, cat}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {sendRequest, dispatch} = useWS();
|
|
||||||
const [trees, setTrees] = useState([]);
|
|
||||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
|
||||||
const combDispatch = useCombsDispatch();
|
|
||||||
|
|
||||||
function readAndConvertMatch(matches, data, combsToAdd) {
|
|
||||||
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
|
|
||||||
if (data.c1)
|
|
||||||
combsToAdd.push(data.c1)
|
|
||||||
if (data.c2)
|
|
||||||
combsToAdd.push(data.c2)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!catId)
|
|
||||||
return;
|
|
||||||
setLoading(1);
|
|
||||||
sendRequest('getFullCategory', catId)
|
|
||||||
.then((data) => {
|
|
||||||
setTrees(data.trees.map(d => from_sendTree(d, true)))
|
|
||||||
|
|
||||||
let matches2 = [];
|
|
||||||
let combsToAdd = [];
|
|
||||||
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
|
||||||
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
|
||||||
|
|
||||||
reducer({type: 'REPLACE_ALL', payload: matches2});
|
|
||||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
|
|
||||||
}).finally(() => setLoading(0))
|
|
||||||
|
|
||||||
const treeListener = ({data}) => {
|
|
||||||
if (data.length < 1 || data[0].categorie !== catId)
|
|
||||||
return
|
|
||||||
setTrees(data.map(d => from_sendTree(d, true)))
|
|
||||||
|
|
||||||
let matches2 = [];
|
|
||||||
let combsToAdd = [];
|
|
||||||
data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
|
||||||
reducer({type: 'REPLACE_TREE', payload: matches2});
|
|
||||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchListener = ({data: datas}) => {
|
|
||||||
for (const data of datas) {
|
|
||||||
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}})
|
|
||||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchOrder = ({data}) => {
|
|
||||||
reducer({type: 'REORDER', payload: data})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMatch = ({data: datas}) => {
|
|
||||||
for (const data of datas)
|
|
||||||
reducer({type: 'REMOVE', payload: data})
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
|
|
||||||
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
|
|
||||||
dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}})
|
|
||||||
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
|
|
||||||
return () => {
|
|
||||||
dispatch({type: 'removeListener', payload: treeListener})
|
|
||||||
dispatch({type: 'removeListener', payload: matchListener})
|
|
||||||
dispatch({type: 'removeListener', payload: matchOrder})
|
|
||||||
dispatch({type: 'removeListener', payload: deleteMatch})
|
|
||||||
}
|
|
||||||
}, [catId]);
|
|
||||||
|
|
||||||
return <ListMatch cat={cat} matches={matches} trees={trees}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListMatch({cat, matches, trees}) {
|
|
||||||
const [type, setType] = useState(1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ((cat.type & type) === 0)
|
|
||||||
setType(cat.type);
|
|
||||||
}, [cat]);
|
|
||||||
|
|
||||||
return <div style={{marginTop: "1em"}}>
|
|
||||||
{cat && cat.type === 3 && <>
|
|
||||||
<ul className="nav nav-tabs">
|
|
||||||
<li className="nav-item">
|
|
||||||
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
|
|
||||||
onClick={_ => setType(1)}>Poule
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
|
|
||||||
onClick={_ => setType(2)}>Tournois
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
{type === 1 && <>
|
|
||||||
<MatchList matches={matches} cat={cat}/>
|
|
||||||
</>}
|
|
||||||
|
|
||||||
{type === 2 && <>
|
|
||||||
<BuildTree treeData={trees} matches={matches}/>
|
|
||||||
</>}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchList({matches, cat}) {
|
|
||||||
const [activeMatch, setActiveMatch] = useState(null)
|
|
||||||
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "A")
|
|
||||||
const publicAffDispatch = usePubAffDispatch();
|
|
||||||
|
|
||||||
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: win(m.scores)}))
|
|
||||||
|
|
||||||
const match = matches.find(m => m.id === activeMatch)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!match) {
|
|
||||||
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
|
|
||||||
} else {
|
|
||||||
publicAffDispatch({
|
|
||||||
type: 'SET_DATA',
|
|
||||||
payload: {
|
|
||||||
c1: match.c1,
|
|
||||||
c2: match.c2,
|
|
||||||
next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [match]);
|
|
||||||
//useEffect(() => {
|
|
||||||
// if (activeMatch !== null)
|
|
||||||
// setActiveMatch(null);
|
|
||||||
//}, [cat])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (match && match.poule !== lice)
|
|
||||||
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id)
|
|
||||||
}, [lice]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (marches2.length === 0)
|
|
||||||
return;
|
|
||||||
if (marches2.some(m => m.id === activeMatch))
|
|
||||||
return;
|
|
||||||
|
|
||||||
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id);
|
|
||||||
}, [matches])
|
|
||||||
|
|
||||||
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
|
||||||
return <>
|
|
||||||
{liceName.length > 1 &&
|
|
||||||
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
|
|
||||||
<label className="input-group-text" htmlFor="selectLice">Lice</label>
|
|
||||||
<select className="form-select" id="selectLice" value={lice} onChange={e => {
|
|
||||||
setLice(e.target.value);
|
|
||||||
localStorage.setItem("cm_lice", e.target.value);
|
|
||||||
}}>
|
|
||||||
{liceName.map((l, index) => (
|
|
||||||
<option key={index} value={l}>{l}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="table-responsive-xxl">
|
|
||||||
<table className="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th>
|
|
||||||
<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">N°</th>
|
|
||||||
<th style={{textAlign: "center"}} scope="col"></th>
|
|
||||||
<th style={{textAlign: "center"}} scope="col">Rouge</th>
|
|
||||||
<th style={{textAlign: "center"}} scope="col">Blue</th>
|
|
||||||
<th style={{textAlign: "center"}} scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="table-group-divider">
|
|
||||||
{marches2.map((m, index) => (
|
|
||||||
<tr key={m.id} className={m.id === activeMatch ? "table-info" : (m.poule === lice ? "" : "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>
|
|
||||||
<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>
|
|
||||||
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
|
|
||||||
<small><CombName combId={m.c1}/></small></td>
|
|
||||||
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
|
|
||||||
<small><CombName combId={m.c2}/></small></td>
|
|
||||||
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match}/></LoadingProvider>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function BuildTree({treeData, matches}) {
|
|
||||||
const scrollRef = useRef(null)
|
|
||||||
const [currentMatch, setCurrentMatch] = useState(null)
|
|
||||||
const {getComb} = useCombs()
|
|
||||||
const publicAffDispatch = usePubAffDispatch();
|
|
||||||
|
|
||||||
const match = matches.find(m => m.id === currentMatch?.matchSelect)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!match) {
|
|
||||||
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined}});
|
|
||||||
} else {
|
|
||||||
publicAffDispatch({type: 'SET_DATA', payload: {c1: match.c1, c2: match.c2}});
|
|
||||||
}
|
|
||||||
}, [match]);
|
|
||||||
const next_match = matches.find(m => m.id === currentMatch?.matchNext)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!next_match) {
|
|
||||||
publicAffDispatch({type: 'SET_DATA', payload: {next: []}});
|
|
||||||
} else {
|
|
||||||
publicAffDispatch({type: 'SET_DATA', payload: {next: [{c1: next_match.c1, c2: next_match.c2}]}});
|
|
||||||
}
|
|
||||||
}, [next_match]);
|
|
||||||
|
|
||||||
function parseTree(data_in) {
|
|
||||||
if (data_in?.data == null)
|
|
||||||
return null
|
|
||||||
|
|
||||||
const matchData = matches.find(m => m.id === data_in.data)
|
|
||||||
const c1 = getComb(matchData?.c1)
|
|
||||||
const c2 = getComb(matchData?.c2)
|
|
||||||
|
|
||||||
|
|
||||||
let node = new TreeNode({
|
|
||||||
...matchData,
|
|
||||||
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)
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTree(data_in) {
|
|
||||||
let out = []
|
|
||||||
for (const din of data_in) {
|
|
||||||
out.push(parseTree(din))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const trees = initTree(treeData);
|
|
||||||
|
|
||||||
const onMatchClick = (rect, matchId, __) => {
|
|
||||||
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickVoid = () => {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
<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}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentMatch?.matchSelect && <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match}/></LoadingProvider>}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ScorePanel({matchId, match}) {
|
|
||||||
const {sendRequest} = useWS()
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
|
|
||||||
const [end, setEnd] = useState(match?.end || false)
|
|
||||||
const [scoreIn, setScoreIn] = useState("")
|
|
||||||
const inputRef = useRef(null)
|
|
||||||
const tableRef = useRef(null)
|
|
||||||
const scoreRef = useRef([])
|
|
||||||
const lastScoreClick = useRef(null)
|
|
||||||
|
|
||||||
const handleScoreClick = (e, round, comb) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const tableRect = tableRef.current.getBoundingClientRect();
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
|
||||||
|
|
||||||
updateScore();
|
|
||||||
|
|
||||||
const sel = inputRef.current;
|
|
||||||
sel.style.top = (rect.y - tableRect.y) + "px";
|
|
||||||
sel.style.left = (rect.x - tableRect.x) + "px";
|
|
||||||
sel.style.width = rect.width + "px";
|
|
||||||
sel.style.height = rect.height + "px";
|
|
||||||
sel.style.display = "block";
|
|
||||||
|
|
||||||
if (round === -1) {
|
|
||||||
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
|
||||||
setScoreIn("");
|
|
||||||
console.log("Setting for new round", maxRound);
|
|
||||||
lastScoreClick.current = {matchId: matchId, round: maxRound, comb};
|
|
||||||
} else {
|
|
||||||
const score = match.scores.find(s => s.n_round === round);
|
|
||||||
setScoreIn((comb === 1 ? score?.s1 : score?.s2) || "");
|
|
||||||
lastScoreClick.current = {matchId: matchId, round, comb};
|
|
||||||
setTimeout(() => inputRef.current.select(), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateScore = () => {
|
|
||||||
if (lastScoreClick.current !== null) {
|
|
||||||
const {matchId, round, comb} = lastScoreClick.current;
|
|
||||||
lastScoreClick.current = null;
|
|
||||||
|
|
||||||
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn);
|
|
||||||
|
|
||||||
const score = match.scores.find(s => s.n_round === round);
|
|
||||||
let newScore;
|
|
||||||
if (score) {
|
|
||||||
if (comb === 1)
|
|
||||||
newScore = {...score, s1: scoreIn_};
|
|
||||||
else
|
|
||||||
newScore = {...score, s2: scoreIn_};
|
|
||||||
|
|
||||||
if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)};
|
|
||||||
if (newScore.s1 === -1000 && newScore.s2 === -1000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Updating score", matchId, newScore);
|
|
||||||
|
|
||||||
setLoading(1)
|
|
||||||
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickVoid = () => {
|
|
||||||
updateScore();
|
|
||||||
|
|
||||||
const sel = inputRef.current;
|
|
||||||
sel.style.display = "none";
|
|
||||||
lastScoreClick.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!match || match?.end === end)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (end) {
|
|
||||||
if (win(match?.scores) === 0 && match.categorie_ord === -42) {
|
|
||||||
toast.error("Impossible de terminer un match nul en tournois.");
|
|
||||||
setEnd(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(1)
|
|
||||||
sendRequest('updateMatchEnd', {matchId: matchId, end})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}, [end]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onClickVoid()
|
|
||||||
}, [matchId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (match?.scores)
|
|
||||||
scoreRef.current = scoreRef.current.slice(0, match.scores.length);
|
|
||||||
}, [match?.scores]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!match)
|
|
||||||
return;
|
|
||||||
setEnd(match.end);
|
|
||||||
}, [match]);
|
|
||||||
|
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
|
||||||
const o = [...tooltipTriggerList]
|
|
||||||
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
|
||||||
|
|
||||||
const tt = "Score speciaux : <br/>" +
|
|
||||||
"-997 : disqualifié <br/>" +
|
|
||||||
"-998 : absent <br/>" +
|
|
||||||
"-999 : forfait"
|
|
||||||
|
|
||||||
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
|
|
||||||
return <div className="row" onClick={onClickVoid}>
|
|
||||||
<div ref={tableRef} className="col" style={{position: "relative"}}>
|
|
||||||
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
|
|
||||||
data-bs-html="true"/></h6>
|
|
||||||
<table className="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style={{textAlign: "center"}} scope="col">Manche</th>
|
|
||||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th>
|
|
||||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="table-group-divider">
|
|
||||||
{match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => (
|
|
||||||
<tr key={score.n_round}>
|
|
||||||
<th style={{textAlign: "center"}}>{score.n_round + 1}</th>
|
|
||||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2] = e}
|
|
||||||
onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)}</td>
|
|
||||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2 + 1] = e}
|
|
||||||
onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
<tr>
|
|
||||||
<th style={{textAlign: "center"}}></th>
|
|
||||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>-</td>
|
|
||||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>-
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div style={{textAlign: "right"}}>
|
|
||||||
<div className="form-check" style={{display: "inline-block"}}>
|
|
||||||
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
|
|
||||||
onChange={e => setEnd(e.target.checked)}/>
|
|
||||||
<label className="form-check-label" htmlFor="checkboxEnd">Terminé</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
|
|
||||||
max="999"
|
|
||||||
value={scoreIn} onChange={e => setScoreIn(e.target.value)}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === "Tab") {
|
|
||||||
if (lastScoreClick.current !== null) {
|
|
||||||
const {round, comb} = lastScoreClick.current;
|
|
||||||
const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1);
|
|
||||||
if (nextIndex >= 0 && nextIndex < scoreRef.current.length) {
|
|
||||||
e.preventDefault();
|
|
||||||
scoreRef.current[nextIndex].click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClickVoid();
|
|
||||||
}
|
|
||||||
}}/>
|
|
||||||
</div>
|
|
||||||
<div className="col">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user