feat: cm table matches & scores
This commit is contained in:
parent
bb901392fc
commit
489bfeb354
@ -7,6 +7,9 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@ -36,4 +39,20 @@ public class TreeModel {
|
||||
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
|
||||
@JoinColumn(referencedColumnName = "id")
|
||||
TreeModel right;
|
||||
|
||||
public List<TreeModel> flat() {
|
||||
List<TreeModel> out = new ArrayList<>();
|
||||
this.flat(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void flat(List<TreeModel> out) {
|
||||
out.add(this);
|
||||
|
||||
if (this.right != null)
|
||||
this.right.flat(out);
|
||||
|
||||
if (this.left != null)
|
||||
this.left.flat(out);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CategoryModel;
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.data.model.*;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSMatch;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
@ -21,9 +20,7 @@ import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@WithSession
|
||||
@ -41,6 +38,9 @@ public class RMatch {
|
||||
@Inject
|
||||
CategoryRepository categoryRepository;
|
||||
|
||||
@Inject
|
||||
TreeRepository treeRepository;
|
||||
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@ -140,6 +140,140 @@ public class RMatch {
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "updateMatchScore", permission = PermLevel.TABLE)
|
||||
public Uni<Void> updateMatchScore(WebSocketConnection connection, MatchScore score) {
|
||||
return getById(score.matchId(), connection)
|
||||
.chain(matchModel -> {
|
||||
int old_win = matchModel.win();
|
||||
Optional<ScoreEmbeddable> optional = matchModel.getScores().stream()
|
||||
.filter(s -> s.getN_round() == score.n_round()).findAny();
|
||||
boolean b = score.s1() != -1000 || score.s2() != -1000;
|
||||
if (optional.isPresent()) {
|
||||
if (b) {
|
||||
optional.get().setS1(score.s1());
|
||||
optional.get().setS2(score.s2());
|
||||
} else {
|
||||
matchModel.getScores().remove(optional.get());
|
||||
}
|
||||
} else if (b) {
|
||||
matchModel.getScores().add(new ScoreEmbeddable(score.n_round(), score.s1(), score.s2()));
|
||||
}
|
||||
|
||||
return Panache.withTransaction(() -> matchRepository.persist(matchModel))
|
||||
.call(mm -> {
|
||||
if (mm.isEnd() && mm.win() != old_win && mm.getCategory_ord() == -42) {
|
||||
return updateEndAndTree(mm, new ArrayList<>())
|
||||
.call(l -> SSMatch.sendMatch(connection, l));
|
||||
}
|
||||
return Uni.createFrom().nullItem();
|
||||
});
|
||||
})
|
||||
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "updateMatchEnd", permission = PermLevel.TABLE)
|
||||
public Uni<Void> updateMatchEnd(WebSocketConnection connection, MatchEnd matchEnd) {
|
||||
List<MatchEntity> toSend = new ArrayList<>();
|
||||
|
||||
return getById(matchEnd.matchId(), connection)
|
||||
.chain(mm -> {
|
||||
if (mm.getCategory_ord() == -42 && mm.win() == 0) { // Tournois
|
||||
mm.setDate(null);
|
||||
mm.setEnd(false);
|
||||
} else {
|
||||
mm.setDate((matchEnd.end) ? new Date() : null);
|
||||
mm.setEnd(matchEnd.end);
|
||||
}
|
||||
return Panache.withTransaction(() -> matchRepository.persist(mm));
|
||||
})
|
||||
.invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
|
||||
.chain(mm -> updateEndAndTree(mm, toSend))
|
||||
.call(__ -> SSMatch.sendMatch(connection, toSend))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
private Uni<List<MatchEntity>> updateEndAndTree(MatchModel mm, List<MatchEntity> toSend) {
|
||||
return (mm.getCategory_ord() != -42) ?
|
||||
Uni.createFrom().item(toSend) :
|
||||
treeRepository.list("category = ?1 AND level != 0", Sort.ascending("level"), mm.getCategory().getId())
|
||||
.chain(treeModels -> {
|
||||
List<TreeModel> node = treeModels.stream().flatMap(t -> t.flat().stream()).toList();
|
||||
List<TreeEntity> trees = treeModels.stream().map(TreeEntity::fromModel).toList();
|
||||
for (int i = 0; i < trees.size() - 1; i++) {
|
||||
TreeEntity.setAssociated(trees.get(i), trees.get(i + 1));
|
||||
}
|
||||
|
||||
TreeEntity root = trees.stream()
|
||||
.filter(t -> t.getMatchNode(mm.getId()) != null)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
TreeEntity currentNode = root.getMatchNode(mm.getId());
|
||||
if (currentNode == null) {
|
||||
LOGGER.error(
|
||||
"currentNode is empty for " + mm.getId() + " in " + mm.getCategory().getId());
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
TreeEntity parent = TreeEntity.getParent(root, currentNode);
|
||||
if (parent == null) {
|
||||
LOGGER.error("parent is empty for " + mm.getId() + " in " + mm.getCategory().getId());
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
int w = mm.win();
|
||||
MembreModel toSetWin = null;
|
||||
MembreModel toSetLose = null;
|
||||
CompetitionGuestModel toSetWinGuest = null;
|
||||
CompetitionGuestModel toSetLoseGuest = null;
|
||||
|
||||
if (mm.isEnd() && w != 0) {
|
||||
toSetWin = (w > 0) ? mm.getC1_id() : mm.getC2_id();
|
||||
toSetLose = (w > 0) ? mm.getC2_id() : mm.getC1_id();
|
||||
toSetWinGuest = (w > 0) ? mm.getC1_guest() : mm.getC2_guest();
|
||||
toSetLoseGuest = (w > 0) ? mm.getC2_guest() : mm.getC1_guest();
|
||||
}
|
||||
|
||||
MatchModel modelWin = node.stream()
|
||||
.filter(n -> Objects.equals(n.getId(), parent.getId()))
|
||||
.findAny()
|
||||
.map(TreeModel::getMatch)
|
||||
.orElseThrow();
|
||||
|
||||
MatchModel modelLose = (parent.getAssociatedNode() == null) ? null : node.stream()
|
||||
.filter(n -> Objects.equals(n.getId(), parent.getAssociatedNode().getId()))
|
||||
.findAny()
|
||||
.map(TreeModel::getMatch)
|
||||
.orElseThrow();
|
||||
|
||||
if (currentNode.equals(parent.getLeft())) {
|
||||
modelWin.setC1_id(toSetWin);
|
||||
modelWin.setC1_guest(toSetWinGuest);
|
||||
if (modelLose != null) {
|
||||
modelLose.setC1_id(toSetLose);
|
||||
modelLose.setC1_guest(toSetLoseGuest);
|
||||
}
|
||||
} else if (currentNode.equals(parent.getRight())) {
|
||||
modelWin.setC2_id(toSetWin);
|
||||
modelWin.setC2_guest(toSetWinGuest);
|
||||
if (modelLose != null) {
|
||||
modelLose.setC2_id(toSetLose);
|
||||
modelLose.setC2_guest(toSetLoseGuest);
|
||||
}
|
||||
}
|
||||
|
||||
return Panache.withTransaction(() -> matchRepository.persist(modelWin)
|
||||
.invoke(mm2 -> toSend.add(MatchEntity.fromModel(mm2)))
|
||||
.call(__ -> modelLose == null ? Uni.createFrom().nullItem() :
|
||||
matchRepository.persist(modelLose)
|
||||
.invoke(mm2 -> toSend.add(MatchEntity.fromModel(mm2)))
|
||||
)
|
||||
.replaceWithVoid());
|
||||
})
|
||||
.map(__ -> toSend);
|
||||
}
|
||||
|
||||
@WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
|
||||
return getById(idMatch, connection)
|
||||
@ -190,6 +324,14 @@ public class RMatch {
|
||||
public record MatchComb(long id, Long c1, Long c2) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record MatchScore(long matchId, int n_round, int s1, int s2) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record MatchEnd(long matchId, boolean end) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record MatchOrder(long id, long pos) {
|
||||
}
|
||||
|
||||
476
src/main/webapp/src/pages/competition/editor/CMTable.jsx
Normal file
476
src/main/webapp/src/pages/competition/editor/CMTable.jsx
Normal file
@ -0,0 +1,476 @@
|
||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.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 {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
||||
import {DrawGraph} from "../../result/DrawGraph.jsx";
|
||||
import {scorePrint, win} from "../../../utils/Tools.js";
|
||||
|
||||
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() {
|
||||
const [catId, setCatId] = useState(-1);
|
||||
|
||||
return <div className="text-center">
|
||||
<div className="row">
|
||||
<div className="col-md-12 col-lg">
|
||||
<div style={{backgroundColor: "#00c700"}}>
|
||||
A
|
||||
</div>
|
||||
<div style={{backgroundColor: "#0099c7"}}>
|
||||
B
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-12 col-xl-6 col-xxl-5">
|
||||
<div className="card">
|
||||
<div className="card-header">Matches</div>
|
||||
<div className="card-body">
|
||||
<CategorieSelect catId={catId} setCatId={setCatId}/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{backgroundColor: "#c70000"}}>
|
||||
D
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
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.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 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)}))
|
||||
|
||||
//useEffect(() => {
|
||||
// if (activeMatch !== null)
|
||||
// setActiveMatch(null);
|
||||
//}, [cat])
|
||||
|
||||
useEffect(() => {
|
||||
if (marches2.length === 0)
|
||||
return;
|
||||
if (marches2.some(m => m.id === activeMatch))
|
||||
return;
|
||||
|
||||
setActiveMatch(marches2.find(m => !m.end)?.id);
|
||||
}, [matches])
|
||||
|
||||
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
||||
return <>
|
||||
<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" : ""} 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} matches={matches}/></LoadingProvider>}
|
||||
</>
|
||||
}
|
||||
|
||||
function BuildTree({treeData, matches}) {
|
||||
const scrollRef = useRef(null)
|
||||
const [currentMatch, setCurrentMatch] = useState(null)
|
||||
const {getComb} = useCombs()
|
||||
|
||||
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)});
|
||||
}
|
||||
|
||||
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}/>
|
||||
</div>
|
||||
|
||||
{currentMatch?.matchSelect && <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matches={matches}/></LoadingProvider>}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
function ScorePanel({matchId, matches}) {
|
||||
const {sendRequest} = useWS()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
|
||||
const match = matches.find(m => m.id === matchId)
|
||||
|
||||
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;
|
||||
|
||||
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>
|
||||
}
|
||||
@ -6,7 +6,7 @@ import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
|
||||
import {DrawGraph} from "../../result/DrawGraph.jsx";
|
||||
import {SelectCombModalContent} from "./SelectCombModalContent.jsx";
|
||||
import {createMatch, scoreToString, win} from "../../../utils/CompetitionTools.js";
|
||||
import {createMatch, scoreToString} from "../../../utils/CompetitionTools.js";
|
||||
|
||||
import {DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core';
|
||||
import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable';
|
||||
@ -15,6 +15,7 @@ import {CSS} from '@dnd-kit/utilities';
|
||||
import {toast} from "react-toastify";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {win} from "../../../utils/Tools.js";
|
||||
|
||||
function CupImg() {
|
||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||
|
||||
@ -5,6 +5,7 @@ import {useWS, WSProvider} from "../../../hooks/useWS.jsx";
|
||||
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
|
||||
import {CMAdmin} from "./CMAdmin.jsx";
|
||||
import {CombsProvider} from "../../../hooks/useComb.jsx";
|
||||
import {CMTable} from "./CMTable.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -43,7 +44,7 @@ function HomeComp() {
|
||||
<Routes>
|
||||
<Route path="/" element={<Home2 perm={perm}/>}/>
|
||||
<Route path="/admin" element={<CMAdmin/>}/>
|
||||
<Route path="/test" element={<Test2/>}/>
|
||||
<Route path="/table" element={<CMTable/>}/>
|
||||
</Routes>
|
||||
</LoadingProvider>
|
||||
</CombsProvider>
|
||||
@ -80,31 +81,18 @@ function WSStatus({setPerm}) {
|
||||
|
||||
function Home2({perm}) {
|
||||
const nav = useNavigate();
|
||||
const {sendRequest} = useWS();
|
||||
|
||||
return <div className="row">
|
||||
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4>
|
||||
<div className="col">
|
||||
{perm === "admin" && <>
|
||||
{perm === "ADMIN" && <>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button>
|
||||
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button>
|
||||
</>}
|
||||
{perm === "table" && <>
|
||||
{perm === "TABLE" && <>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button>
|
||||
</>}
|
||||
</div>
|
||||
<button onClick={() => {
|
||||
sendRequest("getAllMatch", 1156)
|
||||
.then(data => {
|
||||
console.log("Received response for getAllMatch:", data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error in getAllMatch request:", err);
|
||||
})
|
||||
}}>Send Test
|
||||
</button>
|
||||
<button onClick={() => nav(-1)}>Go Back</button>
|
||||
<button onClick={() => nav("test")}>Go Test</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {useEffect, useRef} from "react";
|
||||
import {win} from "../../utils/CompetitionTools.js";
|
||||
import {scorePrint, win} from "../../utils/Tools.js";
|
||||
|
||||
const max_x = 500;
|
||||
const size = 24;
|
||||
@ -18,13 +18,17 @@ export function DrawGraph({
|
||||
onMatchClick = function (rect, match, comb) {
|
||||
},
|
||||
onClickVoid = function () {
|
||||
}
|
||||
},
|
||||
matchSelect = null,
|
||||
matchNext = null,
|
||||
}) {
|
||||
const canvasRef = useRef(null);
|
||||
const actionCanvasRef = useRef(null);
|
||||
const ctxARef = useRef(null);
|
||||
const actionMapRef = useRef({});
|
||||
|
||||
const selectColor = "#30cc30";
|
||||
|
||||
function getBounds(root) {
|
||||
let px = max_x;
|
||||
let py;
|
||||
@ -131,7 +135,7 @@ export function DrawGraph({
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
const score = scores[i].s1 + "-" + scores[i].s2;
|
||||
const score = scorePrint(scores[i].s1) + "-" + scorePrint(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;
|
||||
@ -192,6 +196,20 @@ export function DrawGraph({
|
||||
ctxA.fillRect(pos2.x, pos2.y, pos2.width, pos2.height)
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos2, match: match.id, comb: 2}
|
||||
|
||||
if (matchSelect && match.id === matchSelect) {
|
||||
ctx.strokeStyle = selectColor
|
||||
ctx.strokeRect(px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0), size * 8, size * 2 + (size * 1.5 | 0))
|
||||
ctx.strokeStyle = "#000000"
|
||||
}
|
||||
|
||||
if (matchNext && match.id === matchNext) {
|
||||
ctx.strokeStyle = selectColor
|
||||
ctx.setLineDash([15, 10])
|
||||
ctx.strokeRect(px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0), size * 8, size * 2 + (size * 1.5 | 0))
|
||||
ctx.setLineDash([])
|
||||
ctx.strokeStyle = "#000000"
|
||||
}
|
||||
|
||||
if (max_y.current < py + size + ((size * 1.5 / 2) | 0)) {
|
||||
max_y.current = py + size + (size * 1.5 / 2 | 0);
|
||||
}
|
||||
@ -223,6 +241,23 @@ export function DrawGraph({
|
||||
ctxA.fillRect(pos2.x, pos2.y, pos2.width, pos2.height)
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos2, match: match.id, comb: 2}
|
||||
|
||||
|
||||
if (matchSelect && match.id === matchSelect) {
|
||||
ctx.strokeStyle = selectColor
|
||||
ctx.strokeRect(px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0), size * 8,
|
||||
py + size * 2 * death - (size * 1.5 / 2 | 0) - (py - size * 2 * death - (size * 1.5 / 2 | 0)) + (size * 1.5 | 0))
|
||||
ctx.strokeStyle = "#000000"
|
||||
}
|
||||
|
||||
if (matchNext && match.id === matchNext) {
|
||||
ctx.strokeStyle = selectColor
|
||||
ctx.setLineDash([15, 10])
|
||||
ctx.strokeRect(px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0), size * 8,
|
||||
py + size * 2 * death - (size * 1.5 / 2 | 0) - (py - size * 2 * death - (size * 1.5 / 2 | 0)) + (size * 1.5 | 0))
|
||||
ctx.setLineDash([])
|
||||
ctx.strokeStyle = "#000000"
|
||||
}
|
||||
|
||||
if (max_y.current < py + size * 2 * death + ((size * 1.5 / 2) | 0)) {
|
||||
max_y.current = py + size * 2 * death + ((size * 1.5 / 2 | 0));
|
||||
}
|
||||
|
||||
@ -1,28 +1,6 @@
|
||||
export 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
|
||||
}
|
||||
import {scorePrint} from "./Tools.js";
|
||||
|
||||
export 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)
|
||||
}
|
||||
}
|
||||
if (score === null || score === undefined || score.length === 0)
|
||||
return ""
|
||||
|
||||
|
||||
@ -105,3 +105,28 @@ export function getCatName(cat) {
|
||||
return cat;
|
||||
}
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export function scorePrint(s1) {
|
||||
switch (s1) {
|
||||
case -997:
|
||||
return "disc."
|
||||
case -998:
|
||||
return "abs."
|
||||
case -999:
|
||||
return "for."
|
||||
case -1000:
|
||||
return ""
|
||||
default:
|
||||
return String(s1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,25 @@ TreeNode.prototype = {
|
||||
this.left.__flat(out)
|
||||
if (this.right != null)
|
||||
this.right.__flat(out)
|
||||
},
|
||||
nextMatchTree: function (trees) {
|
||||
const tmpData = {keep_death: -1, keep_data: null}
|
||||
for (const treeNode of trees)
|
||||
this.__nextMatchTree(treeNode, 0, tmpData)
|
||||
return tmpData.keep_data;
|
||||
},
|
||||
__nextMatchTree: function (treeNode, death, tmpData) {
|
||||
if (treeNode == null)
|
||||
return;
|
||||
|
||||
const match = treeNode.data
|
||||
if (!match.end && death > tmpData.keep_death && this.data !== match.id) {
|
||||
tmpData.keep_death = death;
|
||||
tmpData.keep_data = match.id;
|
||||
}
|
||||
|
||||
this.__nextMatchTree(treeNode.left, death + 1, tmpData);
|
||||
this.__nextMatchTree(treeNode.right, death + 1, tmpData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user