From d1c7f37a945966cb87bfe92f2afbf654605ff286 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 3 Sep 2025 18:56:58 +0200 Subject: [PATCH] feat: competition result --- .../ffsaf/data/model/MatchModel.java | 27 ++ .../ffsaf/data/model/RegisterModel.java | 10 + .../ffsaf/domain/service/ResultService.java | 400 ++++++++++++++++++ .../ffsaf/rest/ResultEndpoints.java | 48 +++ .../ffsaf/rest/data/ResultCategoryData.java | 59 +++ .../fr/titionfire/ffsaf/utils/TreeNode.java | 35 ++ src/main/resources/lang/String.properties | 16 + src/main/webapp/public/img/171891.png | Bin 0 -> 12575 bytes src/main/webapp/src/App.jsx | 6 + src/main/webapp/src/components/Nav.jsx | 20 +- .../webapp/src/pages/result/DrawGraph.jsx | 252 +++++++++++ .../webapp/src/pages/result/ResultList.jsx | 49 +++ .../webapp/src/pages/result/ResultRoot.jsx | 26 ++ .../webapp/src/pages/result/ResultView.jsx | 347 +++++++++++++++ 14 files changed, 1294 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java create mode 100644 src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java create mode 100644 src/main/resources/lang/String.properties create mode 100644 src/main/webapp/public/img/171891.png create mode 100644 src/main/webapp/src/pages/result/DrawGraph.jsx create mode 100644 src/main/webapp/src/pages/result/ResultList.jsx create mode 100644 src/main/webapp/src/pages/result/ResultRoot.jsx create mode 100644 src/main/webapp/src/pages/result/ResultView.jsx diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java index a20694e..22765ea 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -53,4 +53,31 @@ public class MatchModel { List 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; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 19f84ab..953a637 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -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; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java new file mode 100644 index 0000000..f8cb994 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -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> 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> 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> 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 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 matchModels_, ResultCategoryData out) { + List matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList(); + + HashMap> 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 matchs = matchEntities.stream() + .sorted(Comparator.comparing(MatchModel::getCategory_ord)) + .map(ResultCategoryData.PouleArrayData::fromModel) + .toList(); + + List 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 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 treeModels, ResultCategoryData out) { + ArrayList> trees = new ArrayList<>(); + treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> { + TreeNode root = new TreeNode<>(); + convertTree(treeModel, root); + trees.add(root); + }); + out.setTrees(trees); + } + + public Uni getAllCombArray(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx) + .chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); + + CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); + + List combs = matchModels.stream() + .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) + .filter(Objects::nonNull) + .distinct() + .map(combName -> { + var builder2 = CombsArrayData.CombsData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + 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 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 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 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 registers = pair.getKey(); + List matchModels = pair.getValue(); + + builder.nb_insc(registers.size()); + + AtomicInteger tt_win = new AtomicInteger(0); + AtomicInteger tt_match = new AtomicInteger(0); + + List combData = registers.stream() + .map(register -> { + + var builder2 = ClubArrayData.CombData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + 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 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 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 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"); + })); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java new file mode 100644 index 0000000..4016887 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java @@ -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> getList() { + return resultService.getList(securityCtx); + } + + @GET + @Path("{uuid}") + public Uni> getCategory(@PathParam("uuid") String uuid) { + return resultService.getCategory(uuid, securityCtx); + } + + @GET + @Path("{uuid}/club") + public Uni getClub(@PathParam("uuid") String uuid) { + return resultService.getClubArray(uuid, securityCtx); + } + + @GET + @Path("{uuid}/comb") + public Uni getComb(@PathParam("uuid") String uuid) { + return resultService.getAllCombArray(uuid, securityCtx); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java new file mode 100644 index 0000000..2054e52 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -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> matchs = new HashMap<>(); + HashMap> rankArray = new HashMap<>(); + ArrayList> 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 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 scores, + boolean end) { + public static TreeData from(MatchModel match) { + return new TreeData(match.getId(), match.getC1Name(), match.getC2Name(), match.getScores(), match.isEnd()); + } + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java b/src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java new file mode 100644 index 0000000..c175692 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java @@ -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 { + private T data; + private TreeNode left; + private TreeNode 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); + } +} diff --git a/src/main/resources/lang/String.properties b/src/main/resources/lang/String.properties new file mode 100644 index 0000000..7e87f97 --- /dev/null +++ b/src/main/resources/lang/String.properties @@ -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 \ No newline at end of file diff --git a/src/main/webapp/public/img/171891.png b/src/main/webapp/public/img/171891.png new file mode 100644 index 0000000000000000000000000000000000000000..4ffcd10772fe3b60c4a5769a155a805cce6ec5ca GIT binary patch literal 12575 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 Tz8i}R;Ag|yEo%x^`Jem`la(1) literal 0 HcmV?d00001 diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index bc53786..1cee2e9 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -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: , children: getCompetitionChildren() }, + { + path: 'result', + element: , + children: getResultChildren() + }, { path: 'me', element: diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 825a6a0..2d5c6c5 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -21,6 +21,7 @@ export function Nav() {
  • Accueil
  • + @@ -41,6 +42,23 @@ function AffiliationMenu() { return
  • Demande d'affiliation
  • } +function CompMenu() { + const {is_authenticated} = useAuth() + + if (!is_authenticated) + return <> + + return
  • + +
      +
    • Inscription
    • +
    • Mes résultats
    • +
    +
  • +} + function ClubMenu() { const {is_authenticated, userinfo} = useAuth() @@ -99,4 +117,4 @@ function LoginMenu() { } -} \ No newline at end of file +} diff --git a/src/main/webapp/src/pages/result/DrawGraph.jsx b/src/main/webapp/src/pages/result/DrawGraph.jsx new file mode 100644 index 0000000..47a88ea --- /dev/null +++ b/src/main/webapp/src/pages/result/DrawGraph.jsx @@ -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 ; +} diff --git a/src/main/webapp/src/pages/result/ResultList.jsx b/src/main/webapp/src/pages/result/ResultList.jsx new file mode 100644 index 0000000..6baccb6 --- /dev/null +++ b/src/main/webapp/src/pages/result/ResultList.jsx @@ -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 <> +
    +
    + {data + ? + : error + ? + : + } +
    +
    + +} + +function MakeCentralPanel({data, navigate}) { + + return <> +
    +

    Compétition:

    +
    + {data.sort((a, b) => new Date(b[2].split('T')[0]) - new Date(a[2].split('T')[0])).map((o) => ( +
  • navigate(`${o[0]}`)}>{o[1]}
  • ))} +
    +
    + +} + +function Def() { + return
    +
  • +
  • +
  • +
  • +
  • +
    +} diff --git a/src/main/webapp/src/pages/result/ResultRoot.jsx b/src/main/webapp/src/pages/result/ResultRoot.jsx new file mode 100644 index 0000000..e95ff48 --- /dev/null +++ b/src/main/webapp/src/pages/result/ResultRoot.jsx @@ -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 <> +

    Résultat

    + + + + +} + +export function getResultChildren() { + return [ + { + path: '', + element: + }, + { + path: ':uuid', + element: + }, + ] +} diff --git a/src/main/webapp/src/pages/result/ResultView.jsx b/src/main/webapp/src/pages/result/ResultView.jsx new file mode 100644 index 0000000..9efe245 --- /dev/null +++ b/src/main/webapp/src/pages/result/ResultView.jsx @@ -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 +} + +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 <> + + + {data ? + : error + ? + : } + +
    +
    + {resultShow && resultShow.type !== undefined && + || resultShow && resultShow === "club" && + || resultShow && resultShow === "comb" && } +
    +
    + +} +// || resultShow && resultShow === "club_all" && + +function MenuBar({data, resultShow, setResultShow}) { + return + + /* +
  • + setResultShow("club_all")}>Clubs +
  • + */ +} + +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 <> + + + + + + + + + + + + {matchs.map((match, idx) => + + + + + + )} + +
    RougeScoresBleu
    {match.red}{match.red_w ? : ""}{scoreToString(match.score)}{match.blue_w ? : ""}{match.blue}
    + +} + +function BuildRankArray({rankArray}) { + return <> + + + + + + + + + + + + + {rankArray.map((row, idx) => + + + + + + + )} + +
    PlaceNomVictoireRatioPoints marquésPoints reçus
    {row.rank}{row.name}{row.win}{row.pointRate.toFixed(3)}{row.pointMake}{row.pointTake}
    + +} + +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 +} + +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 && <> + + } + + {type === 1 && <> + {Object.keys(data.matchs).map(p =>
    + {Object.keys(data.matchs).length > 1 &&

    Poule {p}

    } + + +
    )} + } + + {type === 2 && <> + + } + + +} + +function ClubResult({uuid}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/result/${uuid}/club`, setLoading, 1) + + return <> + {data ? <> +

    Info :

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

    Statistique :

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

    Liste des membres :

    + + + + + + + + + + + + + + + + {data.combs.map((comb, idx) => + + + + + + + + + )} + +
    CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points
    {comb.cat}{comb.name}{comb.w}{comb.l}{comb.ratioVictoire.toFixed(3)}{comb.pointMake}{comb.pointTake}{comb.ratioPoint.toFixed(3)}
    + + + : error + ? + : + } + +} + +/*function ClubAllResult({uuid}) { + return
    + +}*/ + +function CombResult({uuid}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1) + + return <> + {data ? <> +

    Statistique :

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

    Liste des combattants :

    + + + + + + + + + + + + + + + + + {data.combs.map((comb, idx) => + + + + + + + + + + )} + +
    CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points
    {comb.cat}{comb.club}{comb.name}{comb.w}{comb.l}{comb.ratioVictoire.toFixed(3)}{comb.pointMake}{comb.pointTake}{comb.ratioPoint.toFixed(3)}
    + + : error + ? + : + } + +} + +function Def() { + return
    +
  • +
  • +
  • +
  • +
  • +
    +}