feat: competition result

This commit is contained in:
Thibaut Valentin 2025-09-03 18:56:58 +02:00
parent 587173c79f
commit d1c7f37a94
14 changed files with 1294 additions and 1 deletions

View File

@ -53,4 +53,31 @@ public class MatchModel {
List<ScoreEmbeddable> scores = new ArrayList<>();
char poule = 'A';
public String getC1Name() {
if (c1_id == null)
return c1_str;
return c1_id.fname + " " + c1_id.lname;
}
public String getC2Name() {
if (c2_id == null)
return c2_str;
return c2_id.fname + " " + c2_id.lname;
}
public int win() {
int sum = 0;
for (ScoreEmbeddable score : this.getScores()) {
if (score.getS1() == -1000 || score.getS2() == -1000)
continue;
if (score.getS1() > score.getS2())
sum++;
else if (score.getS1() < score.getS2())
sum--;
}
return sum;
}
}

View File

@ -53,4 +53,14 @@ public class RegisterModel {
this.categorie = categorie;
this.club = club;
}
public String getName() {
return membre.fname + " " + membre.lname;
}
public ClubModel getClub2() {
if (club == null)
return membre.club;
return club;
}
}

View File

@ -0,0 +1,400 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.Builder;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
public class ResultService {
@Inject
CompetitionRepository compRepository;
@Inject
RegisterRepository registerRepository;
@Inject
MembreService membreService;
@Inject
CategoryRepository categoryRepository;
@Inject
MatchRepository matchRepository;
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String");
public Uni<List<Object[]>> getList(SecurityCtx securityCtx) {
return membreService.getByAccountId(securityCtx.getSubject())
.chain(m -> registerRepository.list("membre = ?1", m))
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(r -> Mutiny.fetch(r.getCompetition()))
.onItem().transform(r -> new Object[]{r.getCompetition().getUuid(), r.getCompetition().getName(),
r.getCompetition().getDate()})
.collect().asList();
}
public Uni<List<ResultCategoryData>> getCategory(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(m -> categoryRepository.list("compet.uuid = ?1", uuid)
.chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1) OR category IN ?2", //TODO AND
m.getMembre(), cats)))
.map(matchModels -> {
HashMap<Long, List<MatchModel>> map = new HashMap<>();
for (MatchModel matchModel : matchModels) {
if (!map.containsKey(matchModel.getCategory().getId()))
map.put(matchModel.getCategory().getId(), new ArrayList<>());
map.get(matchModel.getCategory().getId()).add(matchModel);
}
return map.values();
})
.onItem()
.transformToMulti(Multi.createFrom()::iterable)
.onItem().call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
.onItem().transform(this::getData)
.collect().asList();
}
private ResultCategoryData getData(List<MatchModel> matchModels) {
ResultCategoryData out = new ResultCategoryData();
CategoryModel categoryModel = matchModels.get(0).getCategory();
out.setName(categoryModel.getName());
out.setType(categoryModel.getType());
getArray2(matchModels, out);
getTree(categoryModel.getTree(), out);
return out;
}
private void getArray2(List<MatchModel> matchModels_, ResultCategoryData out) {
List<MatchModel> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();
HashMap<Character, List<MatchModel>> matchMap = new HashMap<>();
for (MatchModel model : matchModels) {
char g = model.getPoule();
if (!matchMap.containsKey(g))
matchMap.put(g, new ArrayList<>());
matchMap.get(g).add(model);
}
matchMap.forEach((c, matchEntities) -> {
List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream()
.sorted(Comparator.comparing(MatchModel::getCategory_ord))
.map(ResultCategoryData.PouleArrayData::fromModel)
.toList();
List<ResultCategoryData.RankArray> rankArray = matchEntities.stream()
.flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
.distinct()
.map(combName -> {
AtomicInteger w = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger(0);
AtomicInteger pointTake = new AtomicInteger(0);
matchEntities.stream()
.filter(m -> m.isEnd() && (m.getC1Name().equals(combName) || m.getC2Name()
.equals(combName)))
.forEach(matchModel -> {
int win = matchModel.win();
if ((matchModel.getC1Name()
.equals(combName) && win > 0) || matchModel.getC2Name()
.equals(combName) && win < 0)
w.getAndIncrement();
for (ScoreEmbeddable score : matchModel.getScores()) {
if (score.getS1() <= -900 || score.getS2() <= -900)
continue;
if (matchModel.getC1Name().equals(combName)) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
}
});
float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get();
return new ResultCategoryData.RankArray(0, combName, w.get(),
pointMake.get(), pointTake.get(), pointRate);
})
.sorted(Comparator
.comparing(ResultCategoryData.RankArray::getWin)
.thenComparing(ResultCategoryData.RankArray::getPointRate).reversed())
.toList();
out.getMatchs().put(c, matchs);
int lastWin = -1;
float pointRate = 0;
int rank = 0;
for (ResultCategoryData.RankArray rankArray1 : rankArray) {
if (rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) {
lastWin = rankArray1.getWin();
pointRate = rankArray1.getPointRate();
rank++;
}
rankArray1.setRank(rank);
}
out.getRankArray().put(c, rankArray);
});
}
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst) {
dst.setData(ResultCategoryData.TreeData.from(src.getMatch()));
if (src.getLeft() != null) {
dst.setLeft(new TreeNode<>());
convertTree(src.getLeft(), dst.getLeft());
}
if (src.getRight() != null) {
dst.setRight(new TreeNode<>());
convertTree(src.getRight(), dst.getRight());
}
}
private void getTree(List<TreeModel> treeModels, ResultCategoryData out) {
ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> {
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
convertTree(treeModel, root);
trees.add(root);
});
out.setTrees(trees);
}
public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid)
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels -> new Pair<>(registers, matchModels)))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue();
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
List<CombsArrayData.CombsData> combs = matchModels.stream()
.flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
.filter(Objects::nonNull)
.distinct()
.map(combName -> {
var builder2 = CombsArrayData.CombsData.builder();
AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
matchModels.stream()
.filter(m -> m.isEnd() && (m.getC1Name().equals(combName)
|| m.getC2Name().equals(combName)))
.forEach(matchModel -> {
int win = matchModel.win();
if ((combName.equals(matchModel.getC1Name()) && win > 0) ||
combName.equals(matchModel.getC2Name()) && win < 0) {
w.getAndIncrement();
} else {
l.getAndIncrement();
}
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> {
if (combName.equals(matchModel.getC1Name())) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
});
});
Categorie categorie = null;
ClubModel club = null;
Optional<RegisterModel> register = registers.stream()
.filter(r -> r.getName().equals(combName)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie();
club = register.get().getClub2();
}
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(combName);
builder2.w(w.get());
builder2.l(l.get());
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName());
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
return builder2.build();
})
.sorted(Comparator.comparing(CombsArrayData.CombsData::name))
.toList();
builder.nb_insc(combs.size());
builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count());
builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum());
builder.combs(combs);
return builder.build();
})
);
}
@Builder
@RegisterForReflection
public static record CombsArrayData(int nb_insc, int tt_match, long point, List<CombsData> combs) {
@Builder
@RegisterForReflection
public static record CombsData(String cat, String club, String name, int w, int l, float ratioVictoire,
float ratioPoint, int pointMake, int pointTake) {
}
}
public Uni<ClubArrayData> getClubArray(String uuid, SecurityCtx securityCtx) {
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
return hasAccess(uuid, securityCtx)
.invoke(cm_register -> builder.name(cm_register.getClub2().getName()))
.chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid,
cm_register.getClub2())
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels -> new Pair<>(registers, matchModels)))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue();
builder.nb_insc(registers.size());
AtomicInteger tt_win = new AtomicInteger(0);
AtomicInteger tt_match = new AtomicInteger(0);
List<ClubArrayData.CombData> combData = registers.stream()
.map(register -> {
var builder2 = ClubArrayData.CombData.builder();
AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
matchModels.stream()
.filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id())
|| register.getMembre().equals(m.getC2_id())))
.forEach(matchModel -> {
int win = matchModel.win();
if ((register.getMembre()
.equals(matchModel.getC1_id()) && win > 0) ||
register.getMembre()
.equals(matchModel.getC2_id()) && win < 0) {
w.getAndIncrement();
} else {
l.getAndIncrement();
}
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> {
if (register.getMembre()
.equals(matchModel.getC1_id())) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
});
});
Categorie categorie = register.getCategorie();
if (categorie == null)
categorie = register.getMembre().getCategorie();
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(register.getName());
builder2.w(w.get());
builder2.l(l.get());
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
tt_win.addAndGet(w.get());
tt_match.addAndGet(w.get() + l.get());
return builder2.build();
})
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
.toList();
builder.nb_match(tt_match.get());
builder.match_w(tt_win.get());
builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
builder.combs(combData);
return builder.build();
})
);
}
@Builder
@RegisterForReflection
public static record ClubArrayData(String name, int nb_insc, int nb_match, int match_w, float ratioVictoire,
float ratioPoint, int pointMake, int pointTake, List<CombData> combs) {
@Builder
@RegisterForReflection
public static record CombData(String cat, String name, int w, int l, float ratioVictoire,
float ratioPoint, int pointMake, int pointTake) {
}
}
private Uni<RegisterModel> hasAccess(String uuid, SecurityCtx securityCtx) {
return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
.firstResult()
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DForbiddenException("Access denied");
}));
}
private Uni<RegisterModel> hasAccess(Long compId, SecurityCtx securityCtx) {
return registerRepository.find("membre.userId = ?1 AND competition.id = ?2", securityCtx.getSubject(), compId)
.firstResult()
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DForbiddenException("Access denied");
}));
}
}

View File

@ -0,0 +1,48 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.ResultService;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import java.util.List;
@Authenticated
@Path("api/result")
public class ResultEndpoints {
@Inject
ResultService resultService;
@Inject
SecurityCtx securityCtx;
@GET
@Path("list")
public Uni<List<Object[]>> getList() {
return resultService.getList(securityCtx);
}
@GET
@Path("{uuid}")
public Uni<List<ResultCategoryData>> getCategory(@PathParam("uuid") String uuid) {
return resultService.getCategory(uuid, securityCtx);
}
@GET
@Path("{uuid}/club")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid) {
return resultService.getClubArray(uuid, securityCtx);
}
@GET
@Path("{uuid}/comb")
public Uni<ResultService.CombsArrayData> getComb(@PathParam("uuid") String uuid) {
return resultService.getAllCombArray(uuid, securityCtx);
}
}

View File

@ -0,0 +1,59 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import fr.titionfire.ffsaf.utils.TreeNode;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@Data
@NoArgsConstructor
@RegisterForReflection
public class ResultCategoryData {
int type;
String name;
HashMap<Character, List<PouleArrayData>> matchs = new HashMap<>();
HashMap<Character, List<RankArray>> rankArray = new HashMap<>();
ArrayList<TreeNode<TreeData>> trees;
@Data
@AllArgsConstructor
@RegisterForReflection
public static class RankArray {
int rank;
String name;
int win;
int pointMake;
int pointTake;
float pointRate;
}
@RegisterForReflection
public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue, boolean end) {
public static PouleArrayData fromModel(MatchModel matchModel) {
return new PouleArrayData(
matchModel.getC1Name(),
matchModel.isEnd() && matchModel.win() > 0,
matchModel.isEnd() ?
matchModel.getScores().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()
: new ArrayList<>(),
matchModel.isEnd() && matchModel.win() < 0,
matchModel.getC2Name(),
matchModel.isEnd());
}
}
@RegisterForReflection
public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores,
boolean end) {
public static TreeData from(MatchModel match) {
return new TreeData(match.getId(), match.getC1Name(), match.getC2Name(), match.getScores(), match.isEnd());
}
}
}

View File

@ -0,0 +1,35 @@
package fr.titionfire.ffsaf.utils;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
public class TreeNode<T> {
private T data;
private TreeNode<T> left;
private TreeNode<T> right;
public TreeNode(T data) {
this(data, null, null);
}
public int death() {
int dg = 0;
int dd = 0;
if (this.right != null)
dg = this.right.death();
if (this.left != null)
dg = this.left.death();
return 1 + Math.max(dg, dd);
}
}

View File

@ -0,0 +1,16 @@
filtre.all=--tout--
no.licence=Non licenci\u00E9
# Categories
Cat.SUPER_MINI=Super Mini
Cat.MINI_POUSSIN=Mini Poussin
Cat.POUSSIN= Poussin
Cat.BENJAMIN=Benjamin
Cat.MINIME=Minime
Cat.CADET=Cadet
Cat.JUNIOR=Junior
Cat.SENIOR1=Senior 1
Cat.SENIOR2=Senior 2
Cat.VETERAN1=V\u00E9t\u00E9ran 1
Cat.VETERAN2=V\u00E9t\u00E9ran 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -14,6 +14,7 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
const router = createBrowserRouter([
{
@ -53,6 +54,11 @@ const router = createBrowserRouter([
element: <CompetitionRoot/>,
children: getCompetitionChildren()
},
{
path: 'result',
element: <ResultRoot/>,
children: getResultChildren()
},
{
path: 'me',
element: <MePage/>

View File

@ -21,6 +21,7 @@ export function Nav() {
<div className="collapse-item">
<ul className="navbar-nav">
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
<CompMenu/>
<ClubMenu/>
<AdminMenu/>
<AffiliationMenu/>
@ -41,6 +42,23 @@ function AffiliationMenu() {
return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">Demande d'affiliation</NavLink></li>
}
function CompMenu() {
const {is_authenticated} = useAuth()
if (!is_authenticated)
return <></>
return <li className="nav-item dropdown">
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Compétitions
</div>
<ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
</ul>
</li>
}
function ClubMenu() {
const {is_authenticated, userinfo} = useAuth()
@ -99,4 +117,4 @@ function LoginMenu() {
</li>
}
</>
}
}

View File

@ -0,0 +1,252 @@
import {useEffect, useRef} from "react";
const max_x = 500;
const size = 24;
export function DrawGraph({root = []}) {
const canvasRef = useRef(null);
function getBounds(root) {
let px = max_x;
let py;
let maxx, minx, miny, maxy
function drawNode(tree, px, py) {
let death = tree.death() - 1
if (death === 0) {
if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0;
if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0;
} else {
if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0))
miny = py - size * 2 * death - ((size * 1.5 / 2) | 0);
if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0))
maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0);
}
if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8;
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death);
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death);
}
if (root != null) {
py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2;
maxx = px;
minx = px;
miny = py - (size * 1.5 / 2) | 0;
maxy = py + (size * 1.5 / 2) | 0;
for (const node of root) {
px = px - size * 2 - size * 8;
if (minx > px) minx = px;
drawNode(node, px, py);
//graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny);
py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
px = maxx;
}
} else {
minx = 0;
maxx = 0;
miny = 0;
maxy = 0;
}
return [minx, maxx, miny, maxy];
}
// Fonction pour dessiner du texte avec gestion de la taille
const printText = (ctx, s, x, y, width, height, lineG, lineD) => {
ctx.save();
ctx.translate(x, y);
let tSize = 17;
let ratioX = height * 1.0 / 20.0;
ctx.font = "100 " + tSize + "px Arial";
let mw = width - (ratioX * 2) | 0;
if (ctx.measureText(s).width > mw) {
do {
tSize--;
ctx.font = tSize + "px Arial";
} while (ctx.measureText(s).width > mw && tSize > 10);
if (ctx.measureText(s).width > mw) {
let truncated = "";
const words = s.split(" ");
for (const word of words) {
if (ctx.measureText(truncated + word).width >= mw) {
truncated += "...";
break;
} else {
truncated += word + " ";
}
}
s = truncated;
}
}
const text = ctx.measureText(s);
let dx = (width - text.width) / 2;
let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillText(s, dx, dy, width - dy);
ctx.restore();
ctx.beginPath();
if (lineD) {
ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2);
ctx.lineTo(x + width, y + height / 2);
}
if (lineG) {
ctx.moveTo(x, y + height / 2);
ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2);
}
ctx.stroke();
};
// Fonction pour afficher les scores
const printScores = (ctx, scores, px, py, scale) => {
ctx.save();
ctx.translate(px - size * 2, py - size * scale);
ctx.font = "100 14px Arial";
ctx.textBaseline = 'top';
for (let i = 0; i < scores.length; i++) {
const score = scores[i].s1 + "-" + scores[i].s2;
const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3;
const text = ctx.measureText(score);
let dx = (size * 2 - text.width) / 2;
let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillStyle = '#ffffffdd';
ctx.fillRect(dx, size * 2 * scale / div * i + dy, text.width, 14);
ctx.fillStyle = "#000000";
ctx.fillText(score, dx, size * 2 * scale / div * i + dy, size * 2);
}
ctx.restore();
};
// Fonction pour dessiner un nœud
const drawNode = (ctx, tree, px, py, max_y) => {
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px - size, py);
ctx.stroke();
let death = tree.death() - 1;
let match = tree.data;
if (death === 0) {
ctx.beginPath();
ctx.moveTo(px - size, py + size);
ctx.lineTo(px - size, py - size);
ctx.moveTo(px - size, py + size);
ctx.lineTo(px - size * 2, py + size);
ctx.moveTo(px - size, py - size);
ctx.lineTo(px - size * 2, py - size);
ctx.stroke();
printScores(ctx, match.scores, px, py, 1);
ctx.fillStyle = "#FF0000";
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName,
px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0),
size * 8, (size * 1.5 | 0), false, true);
ctx.fillStyle = "#0000FF";
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName,
px - size * 2 - size * 8, py + size - (size * 1.5 / 2 | 0),
size * 8, (size * 1.5 | 0), false, true);
if (max_y.current < py + size + ((size * 1.5 / 2) | 0)) {
max_y.current = py + size + (size * 1.5 / 2 | 0);
}
} else {
ctx.beginPath();
ctx.moveTo(px - size, py);
ctx.lineTo(px - size, py + size * 2 * death);
ctx.moveTo(px - size, py);
ctx.lineTo(px - size, py - size * 2 * death);
ctx.moveTo(px - size, py + size * 2 * death);
ctx.lineTo(px - size * 2, py + size * 2 * death);
ctx.moveTo(px - size, py - size * 2 * death);
ctx.lineTo(px - size * 2, py - size * 2 * death);
ctx.stroke();
printScores(ctx, match.scores, px, py, 1.5);
ctx.fillStyle = "#FF0000";
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName,
px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0),
size * 8, (size * 1.5 | 0), true, true);
ctx.fillStyle = "#0000FF";
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName,
px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2 | 0),
size * 8, (size * 1.5 | 0), true, true);
if (max_y.current < py + size * 2 * death + ((size * 1.5 / 2) | 0)) {
max_y.current = py + size * 2 * death + ((size * 1.5 / 2 | 0));
}
}
if (tree.left != null) {
drawNode(ctx, tree.left, px - size * 2 - size * 8, py - size * 2 * death, max_y);
}
if (tree.right != null) {
drawNode(ctx, tree.right, px - size * 2 - size * 8, py + size * 2 * death, max_y);
}
};
// Fonction pour déterminer le gagnant
const 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;
};
// Effet pour dessiner le graphique
useEffect(() => {
if (root.length === 0) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const [minx, maxx, miny, maxy] = getBounds(root);
canvas.width = maxx - minx;
canvas.height = maxy - miny;
ctx.translate(-minx, -miny);
ctx.fillStyle = "#000000";
ctx.lineWidth = 2;
ctx.strokeStyle = "#000000";
let px = maxx;
let py;
const max_y = {current: 0};
py = (size * 2 * root[0].death() + (((size * 1.5 / 2) | 0) + size) * root[0].death()) * 2;
max_y.current = py + (size * 1.5 / 2 | 0);
for (const node of root) {
let win_name = "";
if (node.data.end) {
win_name = win(node.data.scores) > 0
? (node.data.c1FullName === null ? "???" : node.data.c1FullName)
: (node.data.c2FullName === null ? "???" : node.data.c2FullName);
}
ctx.fillStyle = "#18A918";
printText(ctx, win_name,
px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0),
size * 8, (size * 1.5 | 0), true, false);
px = px - size * 2 - size * 8;
drawNode(ctx, node, px, py, max_y);
py = max_y.current + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
px = maxx;
}
}, [root]);
return <canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px"}} id="myCanvas"></canvas>;
}

View File

@ -0,0 +1,49 @@
import {useNavigate} from "react-router-dom";
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useAuth} from "../../hooks/useAuth.jsx";
export function ResultList() {
const navigate = useNavigate();
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/list`, setLoading, 1)
return <>
<div>
<div className="row">
{data
? <MakeCentralPanel data={data} navigate={navigate}/>
: error
? <AxiosError error={error}/>
: <Def/>
}
</div>
</div>
</>
}
function MakeCentralPanel({data, navigate}) {
return <>
<div className="mb-4">
<h4>Compétition:</h4>
<div className="list-group">
{data.sort((a, b) => new Date(b[2].split('T')[0]) - new Date(a[2].split('T')[0])).map((o) => (
<li className="list-group-item list-group-item-action" key={o[0]}
onClick={e => navigate(`${o[0]}`)}>{o[1]}</li>))}
</div>
</div>
</>
}
function Def() {
return <div className="list-group">
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
</div>
}

View File

@ -0,0 +1,26 @@
import {LoadingProvider} from "../../hooks/useLoading.jsx";
import {Outlet} from "react-router-dom";
import {ResultList} from "./ResultList.jsx";
import {ResultView} from "./ResultView.jsx";
export function ResultRoot() {
return <>
<h1>Résultat</h1>
<LoadingProvider>
<Outlet/>
</LoadingProvider>
</>
}
export function getResultChildren() {
return [
{
path: '',
element: <ResultList/>
},
{
path: ':uuid',
element: <ResultView/>
},
]
}

View File

@ -0,0 +1,347 @@
import {useNavigate, useParams} from "react-router-dom";
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useEffect, useState} from "react";
import {DrawGraph} from "./DrawGraph.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 ResultView() {
const {uuid} = useParams()
const navigate = useNavigate();
const [resultShow, setResultShow] = useState(null)
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}`, setLoading, 1)
return <>
<button type="button" className="btn btn-link" onClick={() => navigate("/result")}>
&laquo; retour
</button>
{data ? <MenuBar data={data} resultShow={resultShow} setResultShow={setResultShow}/>
: error
? <AxiosError error={error}/>
: <Def/>}
<div className="row">
<div className="col-auto">
{resultShow && resultShow.type !== undefined && <PouleResult data={resultShow}/>
|| resultShow && resultShow === "club" && <ClubResult uuid={uuid}/>
|| resultShow && resultShow === "comb" && <CombResult uuid={uuid}/>}
</div>
</div>
</>
}
// || resultShow && resultShow === "club_all" && <ClubAllResult uuid={uuid}/>
function MenuBar({data, resultShow, setResultShow}) {
return <ul className="nav nav-tabs">
<li className="nav-item dropdown">
<a className={"nav-link dropdown-toggle my-1"} data-bs-toggle="dropdown" href="#"
aria-current={(resultShow?.type !== undefined ? " page" : "false")} role="button" aria-expanded="false">Catégorie</a>
<ul className="dropdown-menu">
{data.map(item => <li><a key={item.name} className={"dropdown-item" + (resultShow === item ? " active" : "")}
aria-current={(resultShow === item ? " page" : "false")} href="#" data-bs-toggle="collapse"
onClick={_ => setResultShow(item)}>{item.name}</a></li>)}
</ul>
</li>
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "club" ? " active" : "")} aria-current={(resultShow === "club" ? " page" : "false")}
href="#" onClick={_ => setResultShow("club")}>Mon club</a>
</li>
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "comb" ? " active" : "")} aria-current={(resultShow === "comb" ? " page" : "false")}
href="#" onClick={_ => setResultShow("comb")}>Combattants</a>
</li>
</ul>
/*
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "club_all" ? " active" : "")}
aria-current={(resultShow === "club_all" ? " page" : "false")}
href="#" onClick={_ => setResultShow("club_all")}>Clubs</a>
</li>
*/
}
function scoreToString(score) {
const scorePrint = (s1) => {
switch (s1) {
case -997:
return "disc.";
case -998:
return "abs.";
case -999:
return "for.";
case -1000:
return "";
default:
return String(s1);
}
}
return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ");
}
function BuildMatchArray({matchs}) {
return <>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>Rouge</th>
<th scope="col" style={{textAlign: "center"}}></th>
<th scope="col" style={{textAlign: "center"}}>Scores</th>
<th scope="col" style={{textAlign: "center"}}></th>
<th scope="col" style={{textAlign: "center"}}>Bleu</th>
</tr>
</thead>
<tbody>
{matchs.map((match, idx) => <tr key={idx}>
<td style={{textAlign: "right"}}>{match.red}</td>
<td style={{textAlign: "center"}}>{match.red_w ? <CupImg/> : ""}</td>
<td style={{textAlign: "center"}}>{scoreToString(match.score)}</td>
<td style={{textAlign: "center"}}>{match.blue_w ? <CupImg/> : ""}</td>
<td style={{textAlign: "left"}}>{match.blue}</td>
</tr>)}
</tbody>
</table>
</>
}
function BuildRankArray({rankArray}) {
return <>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>Place</th>
<th scope="col" style={{textAlign: "center"}}>Nom</th>
<th scope="col" style={{textAlign: "center"}}>Victoire</th>
<th scope="col" style={{textAlign: "center"}}>Ratio</th>
<th scope="col" style={{textAlign: "center"}}>Points marqués</th>
<th scope="col" style={{textAlign: "center"}}>Points reçus</th>
</tr>
</thead>
<tbody>
{rankArray.map((row, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{row.rank}</td>
<td style={{textAlign: "left"}}>{row.name}</td>
<td style={{textAlign: "center"}}>{row.win}</td>
<td style={{textAlign: "center"}}>{row.pointRate.toFixed(3)}</td>
<td style={{textAlign: "center"}}>{row.pointMake}</td>
<td style={{textAlign: "center"}}>{row.pointTake}</td>
</tr>)}
</tbody>
</table>
</>
}
function BuildTree({treeData}) {
class TreeNode {
constructor(data) {
this.data = data;
this.left = null;
this.right = null;
}
death() {
let dg = 0;
let dd = 0;
if (this.right != null)
dg = this.right.death();
if (this.left != null)
dg = this.left.death();
return 1 + Math.max(dg, dd);
}
}
function parseTree(data_in) {
if (data_in?.data == null)
return null;
let node = new TreeNode(data_in.data);
node.left = parseTree(data_in?.left);
node.right = parseTree(data_in?.right);
return node;
}
function initTree(data_in) {
let out = [];
for (const din of data_in) {
out.push(parseTree(din));
}
return out;
}
return <DrawGraph root={initTree(treeData)}/>
}
function PouleResult({data}) {
const [type, setType] = useState(data.type === 3 ? 1 : data.type)
useEffect(() => {
setType(data.type === 3 ? 1 : data.type)
}, [data]);
return <>
{data.type === 3 && <>
<ul className="nav nav-tabs">
<li className="nav-item">
<a className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")} href="#"
onClick={_ => setType(1)}>Poule</a>
</li>
<li className="nav-item">
<a className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} href="#"
onClick={_ => setType(2)}>Tournois</a>
</li>
</ul>
</>}
{type === 1 && <>
{Object.keys(data.matchs).map(p => <div key={p}>
{Object.keys(data.matchs).length > 1 && <h4 style={{marginTop: "2em"}}>Poule {p}</h4>}
<BuildMatchArray matchs={data.matchs[p]}/>
<BuildRankArray rankArray={data.rankArray[p]}/>
</div>)}
</>}
{type === 2 && <>
<BuildTree treeData={data.trees}/>
</>}
</>
}
function ClubResult({uuid}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/club`, setLoading, 1)
return <>
{data ? <>
<h3>Info :</h3>
<ul>
<li>Nom : {data.name}</li>
<li>Nombre d'inscris : {data.nb_insc}</li>
</ul>
<h3>Statistique :</h3>
<ul>
<li>Nombre de match disput&eacute; : {data.nb_match}</li>
<li>Nombre de victoires : {data.match_w}</li>
<li>Ratio de victoires moyen : {data.ratioVictoire.toFixed(3)}</li>
<li>Points marqués : {data.pointMake}</li>
<li>Points reçus : {data.pointTake}</li>
<li>Ratio de points moyen : {data.ratioPoint.toFixed(3)}</li>
</ul>
<h3>Liste des membres :</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>Cat&eacute;gorie</th>
<th scope="col" style={{textAlign: "center"}}>Nom</th>
<th scope="col" style={{textAlign: "center"}}>Victoires</th>
<th scope="col" style={{textAlign: "center"}}>D&eacute;faites</th>
<th scope="col" style={{textAlign: "center"}}>Ratio victoires</th>
<th scope="col" style={{textAlign: "center"}}>Points marqués</th>
<th scope="col" style={{textAlign: "center"}}>Points reçus</th>
<th scope="col" style={{textAlign: "center"}}>Ratio points</th>
</tr>
</thead>
<tbody>
{data.combs.map((comb, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{comb.cat}</td>
<td style={{textAlign: "center"}}>{comb.name}</td>
<td style={{textAlign: "center"}}>{comb.w}</td>
<td style={{textAlign: "center"}}>{comb.l}</td>
<td style={{textAlign: "center"}}>{comb.ratioVictoire.toFixed(3)}</td>
<td style={{textAlign: "center"}}>{comb.pointMake}</td>
<td style={{textAlign: "center"}}>{comb.pointTake}</td>
<td style={{textAlign: "center"}}>{comb.ratioPoint.toFixed(3)}</td>
</tr>)}
</tbody>
</table>
</>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
/*function ClubAllResult({uuid}) {
return <div></div>
}*/
function CombResult({uuid}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1)
return <>
{data ? <>
<h3>Statistique :</h3>
<ul>
<li>Nombre d'inscris : {data.nb_insc}</li>
<li>Nombre de match disput&eacute; : {data.tt_match}</li>
<li>Points marqués : {data.point}</li>
</ul>
<h3>Liste des combattants :</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>Cat&eacute;gorie</th>
<th scope="col" style={{textAlign: "center"}}>Club</th>
<th scope="col" style={{textAlign: "center"}}>Nom</th>
<th scope="col" style={{textAlign: "center"}}>Victoires</th>
<th scope="col" style={{textAlign: "center"}}>D&eacute;faites</th>
<th scope="col" style={{textAlign: "center"}}>Ratio victoires</th>
<th scope="col" style={{textAlign: "center"}}>Points marqués</th>
<th scope="col" style={{textAlign: "center"}}>Points reçus</th>
<th scope="col" style={{textAlign: "center"}}>Ratios points</th>
</tr>
</thead>
<tbody>
{data.combs.map((comb, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{comb.cat}</td>
<td style={{textAlign: "center"}}>{comb.club}</td>
<td style={{textAlign: "center"}}>{comb.name}</td>
<td style={{textAlign: "center"}}>{comb.w}</td>
<td style={{textAlign: "center"}}>{comb.l}</td>
<td style={{textAlign: "center"}}>{comb.ratioVictoire.toFixed(3)}</td>
<td style={{textAlign: "center"}}>{comb.pointMake}</td>
<td style={{textAlign: "center"}}>{comb.pointTake}</td>
<td style={{textAlign: "center"}}>{comb.ratioPoint.toFixed(3)}</td>
</tr>)}
</tbody>
</table>
</>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function Def() {
return <div className="list-group">
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
</div>
}