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 0000000..4ffcd10 Binary files /dev/null and b/src/main/webapp/public/img/171891.png differ 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
    +
  • +
  • +
  • +
  • +
  • +
    +}