From e2197d0712c4ba3920fcc03a228a4121a3f87898 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sun, 1 Feb 2026 15:25:49 +0100 Subject: [PATCH 1/8] fix: score panel fonction update --- .../src/pages/competition/editor/ScoreAndCardPanel.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx b/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx index be41709..053ff58 100644 --- a/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx @@ -74,7 +74,8 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_, vEnd}) lastScoreClick.current = {matchId: matchId, round: maxRound, comb}; } else { const score = match.scores.find(s => s.n_round === round); - setScoreIn((comb === 1 ? score?.s1 : score?.s2) || ""); + const tmp= (comb === 1 ? score?.s1 : score?.s2) + setScoreIn((tmp === -1000 ? "" : tmp) || ""); lastScoreClick.current = {matchId: matchId, round, comb}; setTimeout(() => inputRef.current.select(), 100); } @@ -165,7 +166,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_, vEnd}) return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, []); + }, [matchId, match]); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') const o = [...tooltipTriggerList] -- 2.49.0 From 89d9e04a6f011cf3c9a61f0e54b70f25a19239ea Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 5 Feb 2026 21:29:27 +0100 Subject: [PATCH 2/8] feat: add classement match system --- .../ffsaf/data/model/CategoryModel.java | 6 + .../ffsaf/data/model/MatchModel.java | 16 ++ .../ffsaf/data/model/TreeModel.java | 48 +++++ .../data/repository/MatchRepository.java | 3 + .../ffsaf/domain/entity/MatchModelExtend.java | 8 + .../ffsaf/domain/service/ResultService.java | 64 ++++++- .../ffsaf/domain/service/TradService.java | 11 ++ .../ffsaf/rest/data/ResultCategoryData.java | 24 ++- .../fr/titionfire/ffsaf/ws/recv/RCard.java | 6 +- .../titionfire/ffsaf/ws/recv/RCategorie.java | 177 +++++++++++++++++- .../fr/titionfire/ffsaf/ws/recv/RMatch.java | 12 +- .../resources/lang/messages_en.properties | 2 + .../resources/lang/messages_fr.properties | 4 +- src/main/webapp/public/competition.js | 36 +++- src/main/webapp/public/locales/en/cm.json | 8 + src/main/webapp/public/locales/en/result.json | 4 +- src/main/webapp/public/locales/fr/cm.json | 8 + src/main/webapp/public/locales/fr/result.json | 4 +- .../src/pages/competition/editor/CMAdmin.jsx | 63 ++++--- .../competition/editor/CMTMatchPanel.jsx | 139 ++++++++++---- .../src/pages/competition/editor/CMTable.jsx | 2 +- .../editor/CategoryAdminContent.jsx | 99 +++++++--- .../webapp/src/pages/result/ResultView.jsx | 47 ++++- src/main/webapp/src/utils/Tools.js | 14 +- 24 files changed, 678 insertions(+), 127 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java index de7720b..2f31ffc 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java @@ -45,6 +45,12 @@ public class CategoryModel { String liceName = "1"; + @Column(nullable = false, columnDefinition = "boolean default false") + boolean treeAreClassement = false; + + @Column(nullable = false, columnDefinition = "boolean default false") + boolean fullClassement = false; + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "id_preset", referencedColumnName = "id") CatPresetModel preset; 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 c5a6c2f..3c3a3d1 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -109,6 +109,22 @@ public class MatchModel { return sum; } + public CombModel getC1() { + if (this.c1_id != null) + return this.c1_id; + if (this.c1_guest != null) + return this.c1_guest; + return null; + } + + public CombModel getC2() { + if (this.c2_id != null) + return this.c2_id; + if (this.c2_guest != null) + return this.c2_guest; + return null; + } + public boolean isC1(Object comb) { if (this.c1_guest != null && this.c1_guest.isInTeam(comb)) return true; diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java index 2f0e556..d6bf942 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/TreeModel.java @@ -40,6 +40,14 @@ public class TreeModel { @JoinColumn(referencedColumnName = "id") TreeModel right; + public TreeModel(Long category, Integer level, MatchModel match) { + this.category = category; + this.level = level; + this.match = match; + this.left = null; + this.right = null; + } + public List flat() { List out = new ArrayList<>(); this.flat(out); @@ -55,4 +63,44 @@ public class TreeModel { if (this.left != null) this.left.flat(out); } + + 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); + } + + public int getMaxChildrenAtDepth(int death, int current) { + if (current == death) + return 1; + + int tmp = 0; + if (this.right != null) + tmp += this.right.getMaxChildrenAtDepth(death, current + 1); + + if (this.left != null) + tmp += this.left.getMaxChildrenAtDepth(death, current + 1); + + return tmp; + } + + public void getChildrenAtDepth (int death, int current, List out) { + if (current == death) { + out.add(this); + return; + } + + if (this.right != null) + this.right.getChildrenAtDepth(death, current + 1, out); + + if (this.left != null) + this.left.getChildrenAtDepth(death, current + 1, out); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java index 46044b7..acbd34c 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java @@ -20,6 +20,9 @@ public class MatchRepository implements PanacheRepositoryBase } public Uni create(List matchModel) { + if (matchModel.isEmpty()) + return Uni.createFrom().voidItem(); + matchModel.forEach(model -> model.setSystem(CompetitionSystem.INTERNAL)); return Panache.withTransaction(() -> this.persist(matchModel) .call(__ -> this.flush()) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java index f46dff4..6fa0578 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java @@ -173,4 +173,12 @@ public class MatchModelExtend { public boolean isC2(Object comb) { return match.isC2(comb); } + + public CombModel getC1() { + return match.getC1(); + } + + public CombModel getC2() { + return match.getC2(); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index 710f7c3..c3d4fbd 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -151,14 +151,16 @@ public class ResultService { out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName() .split(";")); out.setGenTime(System.currentTimeMillis()); + out.setTreeIsClassement(categoryModel.isTreeAreClassement()); getArray2(matchModels, membreModel, out); getTree(categoryModel.getTree(), membreModel, cards, out); + getClassementArray(categoryModel, membreModel, cards, out); return out; }); } - private void getArray2(List matchModels_, MembreModel membreModel, ResultCategoryData out) { + public void getArray2(List matchModels_, MembreModel membreModel, ResultCategoryData out) { List matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList(); HashMap> matchMap = new HashMap<>(); @@ -182,7 +184,7 @@ public class ResultService { .filter(Objects::nonNull) .map(comb -> { CombStat stat = makeStat(matchEntities, comb); - return new ResultCategoryData.RankArray(0, + return new ResultCategoryData.RankArray(0, comb, comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w, stat.pointMake, stat.pointTake, stat.getPointRate()); }) @@ -210,10 +212,66 @@ public class ResultService { }); } + private void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List cards, + ResultCategoryData out) { + if ((categoryModel.getType() & 2) != 0) { + AtomicInteger rank = new AtomicInteger(0); + categoryModel.getTree().stream() + .filter(t -> t.getLevel() > 0) + .sorted(Comparator.comparing(TreeModel::getLevel)) + .forEach(treeModel -> makeClassementRow(membreModel, + new MatchModelExtend(treeModel.getMatch(), cards), out, rank)); + + categoryModel.getTree().stream() + .filter(t -> t.getLevel() <= -10) + .sorted(Comparator.comparing(TreeModel::getLevel).reversed()) + .forEach(treeModel -> makeClassementRow(membreModel, + new MatchModelExtend(treeModel.getMatch(), cards), out, rank)); + } else { + for (List list : out.getRankArray().values()) { + for (ResultCategoryData.RankArray r : list) { + out.getClassement().add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName())); + } + } + } + } + + private static void makeClassementRow(MembreModel membreModel, MatchModelExtend m, ResultCategoryData out, + AtomicInteger rank) { + if (m.isEnd()) { + if (m.getWin() > 0) { + out.getClassement() + .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(), + m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); + out.getClassement() + .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(), + m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); + } else if (m.getWin() < 0) { + out.getClassement() + .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(), + m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); + out.getClassement() + .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(), + m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); + } else { + out.getClassement() + .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(), + m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); + out.getClassement() + .add(new ResultCategoryData.ClassementData(rank.getAndIncrement(), m.getC2(), + m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); + } + } else { + out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null)); + out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null)); + } + } + private static void convertTree(TreeModel src, TreeNode dst, MembreModel membreModel, ResultPrivacy privacy, List cards) { dst.setData( - ResultCategoryData.TreeData.from(new MatchModelExtend(src.getMatch(), cards), membreModel, privacy)); + ResultCategoryData.TreeData.from(new MatchModelExtend(src.getMatch(), cards), src.getLevel(), + membreModel, privacy)); if (src.getLeft() != null) { dst.setLeft(new TreeNode<>()); convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy, cards); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java index c1102ab..911e814 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java @@ -1,10 +1,12 @@ package fr.titionfire.ffsaf.domain.service; +import io.quarkus.websockets.next.WebSocketConnection; import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; +import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; @@ -21,11 +23,20 @@ public class TradService { return translate(key); } + public String t(String key, WebSocketConnection connection) { + List lang = connection.handshakeRequest().headers().get("Accept-Language"); + Locale userLocale = lang != null && !lang.isEmpty() ? Locale.forLanguageTag(lang.get(0)) : fallbackLocale; + return translate(key, userLocale); + } + public String translate(String key) { ContainerRequestContext requestContext = requestContextInstance.get(); Locale userLocale = (Locale) requestContext.getProperty("userLocale"); + return translate(key, userLocale); + } + public String translate(String key, Locale userLocale) { try { ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale); return messages.getString(key); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java index e99f786..cd0b3a0 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -1,5 +1,7 @@ package fr.titionfire.ffsaf.rest.data; +import com.fasterxml.jackson.annotation.JsonIgnore; +import fr.titionfire.ffsaf.data.model.CombModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.domain.entity.MatchModelExtend; import fr.titionfire.ffsaf.utils.ResultPrivacy; @@ -21,9 +23,11 @@ import java.util.List; public class ResultCategoryData { int type; String name; + boolean treeIsClassement; HashMap> matchs = new HashMap<>(); HashMap> rankArray = new HashMap<>(); ArrayList> trees; + List classement = new ArrayList<>(); String[] liceName; long genTime; @@ -32,6 +36,8 @@ public class ResultCategoryData { @RegisterForReflection public static class RankArray { int rank; + @JsonIgnore + CombModel comb; String name; int score; int win; @@ -43,12 +49,14 @@ public class ResultCategoryData { @RegisterForReflection public record PouleArrayData(String red, boolean red_w, List score, boolean blue_w, String blue, boolean eq, boolean end, Date date) { - public static PouleArrayData fromModel(MatchModelExtend matchModel, MembreModel membreModel, ResultPrivacy privacy) { + public static PouleArrayData fromModel(MatchModelExtend matchModel, MembreModel membreModel, + ResultPrivacy privacy) { return new PouleArrayData( matchModel.getC1Name(membreModel, privacy), matchModel.isEnd() && matchModel.getWin() > 0, matchModel.isEnd() ? - matchModel.getScoresToPrint().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList() + matchModel.getScoresToPrint().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}) + .toList() : new ArrayList<>(), matchModel.isEnd() && matchModel.getWin() < 0, matchModel.getC2Name(membreModel, privacy), @@ -60,10 +68,16 @@ public class ResultCategoryData { @RegisterForReflection public static record TreeData(long id, String c1FullName, String c2FullName, List scores, - boolean end, int win) { - public static TreeData from(MatchModelExtend match, MembreModel membreModel, ResultPrivacy privacy) { + boolean end, int win, int level, Date date) { + public static TreeData from(MatchModelExtend match, int level, MembreModel membreModel, ResultPrivacy privacy) { return new TreeData(match.getId(), match.getC1Name(membreModel, privacy), - match.getC2Name(membreModel, privacy), match.getScoresToPrint(), match.isEnd(), match.getWin()); + match.getC2Name(membreModel, privacy), match.getScoresToPrint(), match.isEnd(), match.getWin(), + level, match.getDate()); } } + + @RegisterForReflection + public static record ClassementData(int rank, @JsonIgnore CombModel comb, String name) { + + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java index 9c82819..94060d8 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java @@ -51,9 +51,9 @@ public class RCard { return matchRepository.findById(id) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException(trad.t("matche.non.trouver")); + throw new DNotFoundException(trad.t("matche.non.trouver", connection)); if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException(trad.t("permission.denied")); + throw new DForbiddenException(trad.t("permission.denied", connection)); })); } @@ -103,7 +103,7 @@ public class RCard { .firstResult() .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException(trad.t("carton.non.trouver")); + throw new DNotFoundException(trad.t("carton.non.trouver", connection)); SSCard.sendRmCards(connection, List.of(o.getId())); })) .chain(cardModel -> Panache.withTransaction(() -> cardRepository.delete(cardModel))) diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index dc977a4..005dcdb 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -1,22 +1,25 @@ package fr.titionfire.ffsaf.ws.recv; -import fr.titionfire.ffsaf.data.model.CardModel; -import fr.titionfire.ffsaf.data.model.CategoryModel; -import fr.titionfire.ffsaf.data.model.MatchModel; -import fr.titionfire.ffsaf.data.model.TreeModel; +import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.domain.entity.MatchEntity; +import fr.titionfire.ffsaf.domain.entity.MatchModelExtend; import fr.titionfire.ffsaf.domain.entity.TreeEntity; import fr.titionfire.ffsaf.domain.service.CardService; +import fr.titionfire.ffsaf.domain.service.ResultService; import fr.titionfire.ffsaf.domain.service.TradService; +import fr.titionfire.ffsaf.rest.data.PresetData; +import fr.titionfire.ffsaf.rest.data.ResultCategoryData; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.utils.TreeNode; import fr.titionfire.ffsaf.ws.PermLevel; import fr.titionfire.ffsaf.ws.send.SSCategorie; +import fr.titionfire.ffsaf.ws.send.SSMatch; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.websockets.next.UserData; import io.quarkus.websockets.next.WebSocketConnection; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; @@ -25,8 +28,8 @@ import jakarta.inject.Inject; import lombok.Data; import org.hibernate.reactive.mutiny.Mutiny; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.stream.Stream; @WithSession @ApplicationScoped @@ -49,6 +52,12 @@ public class RCategorie { @Inject CardService cardService; + @Inject + CardRepository cardRepository; + + @Inject + ResultService resultService; + @Inject TradService trad; @@ -56,9 +65,9 @@ public class RCategorie { return categoryRepository.findById(id) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException(trad.t("categorie.non.trouver")); + throw new DNotFoundException(trad.t("categorie.non.trouver", connection)); if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException(trad.t("permission.denied")); + throw new DForbiddenException(trad.t("permission.denied", connection)); })); } @@ -78,6 +87,9 @@ public class RCategorie { fullCategory.setName(cat.getName()); fullCategory.setLiceName(cat.getLiceName()); fullCategory.setType(cat.getType()); + fullCategory.setTreeAreClassement(cat.isTreeAreClassement()); + fullCategory.setFullClassement(cat.isFullClassement()); + fullCategory.setPreset(PresetData.fromModel(cat.getPreset())); }) .call(cat -> Mutiny.fetch(cat.getMatchs()) .map(matchModels -> matchModels.stream().filter(o -> o.getCategory_ord() >= 0) @@ -116,6 +128,9 @@ public class RCategorie { cat.setName(categorie.name); cat.setLiceName(categorie.liceName); cat.setType(categorie.type); + cat.setTreeAreClassement(categorie.treeAreClassement); + cat.setFullClassement(categorie.fullClassement); + // cat.setPreset(cat.getPreset()); //TODO preset update return Panache.withTransaction(() -> categoryRepository.persist(cat)); }) .call(cat -> { @@ -224,10 +239,149 @@ public class RCategorie { .replaceWithVoid(); } + @WSReceiver(code = "createClassementMatchs", permission = PermLevel.TABLE) + public Uni createClassementMatchs(WebSocketConnection connection, Long categoryId) { + return getById(categoryId, connection) + .call(cat -> { + PermLevel perm = PermLevel.valueOf(connection.userData().get(UserData.TypedKey.forString("prem"))); + if (perm == PermLevel.TABLE) { + return matchRepository.list("category = ?1 AND category_ord = -42", cat.getId()) + .chain(l -> l.stream().anyMatch(MatchModel::isEnd) ? + Uni.createFrom().failure( + new DForbiddenException(trad.t("err.match.termine", connection))) : + Uni.createFrom().voidItem()); + } + return Uni.createFrom().voidItem(); + }) + .call(cat -> treeRepository.list("category = ?1 AND level <= -10", cat.getId()) + .map(l -> l.stream().map(o -> o.getMatch().getId()).toList()) + .call(__ -> treeRepository.delete("category = ?1 AND level <= -10", cat.getId())) + .call(ids -> matchRepository.delete("id IN ?1", ids))) + .call(cat -> Mutiny.fetch(cat.getTree())) + .call(cat -> { + List toSave = new ArrayList<>(); + if (!cat.getTree().isEmpty()) { + for (TreeModel treeModel : cat.getTree()) + cleanTree(treeModel, toSave); + } + return Panache.withTransaction(() -> matchRepository.persist(toSave)) + .invoke(__ -> SSMatch.sendMatch(connection, + toSave.stream().map(MatchEntity::fromModel).toList())); + }) + .chain(cat -> Mutiny.fetch(cat.getMatchs()) + .chain(list -> cardRepository.list("competition = ?1", cat.getCompet()) + .map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList())) + .map(matchModels -> { + ResultCategoryData out = new ResultCategoryData(); + resultService.getArray2(matchModels, null, out); + out.getRankArray().remove('-'); + return out; + }) + .invoke(Unchecked.consumer(result -> { + if (result.getRankArray().size() != 2) { + throw new DForbiddenException(trad.t("configuration.non.supportee", connection)); + } + })) + .chain(result -> { + List toSave = new ArrayList<>(); + List toCreate = new ArrayList<>(); + List toSaveTree = new ArrayList<>(); + + int treeSize = 0; + List lastNode = new ArrayList<>(); + Optional tree = cat.getTree().stream().filter(t -> t.getLevel() > 0) + .min(Comparator.comparing(TreeModel::getLevel)); + if (tree.isPresent()) { + tree.get().getChildrenAtDepth(tree.get().death() - 1, 0, lastNode); + treeSize = lastNode.size(); + } + + Iterator> iterator = result.getRankArray().values() + .iterator(); + List poule1 = iterator.next(); + List poule2 = iterator.next(); + + int maxToMixFill = Math.min(poule1.size(), poule2.size()); + int maxToMixTreeFill = Math.min(treeSize, maxToMixFill); + for (int i = 0; i < maxToMixTreeFill; i++) { + CombModel comb1 = poule1.get(i).getComb(); + CombModel comb2 = poule2.get(treeSize - i - 1).getComb(); + + fillMatchComb(lastNode.get(i).getMatch(), comb1, comb2); + toSave.add(lastNode.get(i).getMatch()); + } + + if (cat.isFullClassement()) { + for (int i = maxToMixTreeFill; i < maxToMixFill; i++) { + MatchModel match = new MatchModel(); + match.setCategory(cat); + match.setCategory_ord(-42); + match.setEnd(false); + + CombModel comb1 = poule1.get(i).getComb(); + CombModel comb2 = poule2.get(i).getComb(); + + fillMatchComb(match, comb1, comb2); + toCreate.add(match); + + toSaveTree.add(new TreeModel(cat.getId(), -10 - i, match)); + } + } + + return Panache.withTransaction(() -> matchRepository.persist(toSave) + .call(__ -> matchRepository.create(toCreate))) + .call(__ -> toSaveTree.isEmpty() ? Uni.createFrom().voidItem() + : Panache.withTransaction(() -> treeRepository.persist(toSaveTree))) + .map(__ -> Stream.concat(toSave.stream(), toCreate.stream()) + .map(MatchEntity::fromModel).toList()); + }) + ) + .onFailure().invoke(t -> System.out.println("error: " + t.getMessage())) + .invoke(matchEntities -> SSMatch.sendMatch(connection, matchEntities)) + .call(__ -> treeRepository.list("category = ?1 AND level != 0", categoryId) + .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) + .invoke(trees -> SSCategorie.sendTreeCategory(connection, trees))) + .replaceWithVoid(); + } + + private void cleanTree(TreeModel treeModel, List toSave) { + MatchModel model = treeModel.getMatch(); + if (model != null) { + model.setC1_id(null); + model.setC1_guest(null); + model.setC2_id(null); + model.setC2_guest(null); + model.setEnd(false); + model.setDate(null); + model.getScores().clear(); + + toSave.add(model); + } + if (treeModel.getLeft() != null) + cleanTree(treeModel.getLeft(), toSave); + if (treeModel.getRight() != null) + cleanTree(treeModel.getRight(), toSave); + } + + private void fillMatchComb(MatchModel match, CombModel comb1, CombModel comb2) { + if (comb1 instanceof MembreModel m) + match.setC1_id(m); + else if (comb1 instanceof CompetitionGuestModel g) + match.setC1_guest(g); + + if (comb2 instanceof MembreModel m) + match.setC2_id(m); + else if (comb2 instanceof CompetitionGuestModel g) + match.setC2_guest(g); + } + + @RegisterForReflection - public record JustCategorie(long id, String name, int type, String liceName) { + public record JustCategorie(long id, String name, int type, String liceName, boolean treeAreClassement, + boolean fullClassement, PresetData preset) { public static JustCategorie from(CategoryModel m) { - return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName()); + return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName(), m.isTreeAreClassement(), + m.isFullClassement(), PresetData.fromModel(m.getPreset())); } } @@ -242,6 +396,9 @@ public class RCategorie { String name; int type; String liceName; + boolean treeAreClassement = false; + boolean fullClassement = false; + PresetData preset; List trees = null; List matches; List cards; diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java index 3490f56..4d3181b 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -55,9 +55,9 @@ public class RMatch { return matchRepository.findById(id) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException(trad.t("matche.non.trouver")); + throw new DNotFoundException(trad.t("matche.non.trouver", connection)); if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException(trad.t("permission.denied")); + throw new DForbiddenException(trad.t("permission.denied", connection)); })); } @@ -85,9 +85,9 @@ public class RMatch { return categoryRepository.findById(m.categorie) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException(trad.t("categorie.non.trouver")); + throw new DNotFoundException(trad.t("categorie.non.trouver", connection)); if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException(trad.t("permission.denied")); + throw new DForbiddenException(trad.t("permission.denied", connection)); })) .chain(categoryModel -> creatMatch(categoryModel, m)) .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))) @@ -297,9 +297,9 @@ public class RMatch { return categoryRepository.findById(data.categorie) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException(trad.t("categorie.non.trouver")); + throw new DNotFoundException(trad.t("categorie.non.trouver", connection)); if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException(trad.t("permission.denied")); + throw new DForbiddenException(trad.t("permission.denied", connection)); })) .call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() : (Panache.withTransaction( diff --git a/src/main/resources/lang/messages_en.properties b/src/main/resources/lang/messages_en.properties index 76556ed..14ee869 100644 --- a/src/main/resources/lang/messages_en.properties +++ b/src/main/resources/lang/messages_en.properties @@ -87,3 +87,5 @@ licence.membre.n.3.inconnue=License member no. 3 unknown demande.d.affiliation.non.trouve=Affiliation request not found carton.non.trouver=Card not found card.cannot.be.added=Unable to add the card +configuration.non.supportee=Unsupported configuration +err.match.termine=Error, a placement match has already been played diff --git a/src/main/resources/lang/messages_fr.properties b/src/main/resources/lang/messages_fr.properties index d430dfd..ac73a7f 100644 --- a/src/main/resources/lang/messages_fr.properties +++ b/src/main/resources/lang/messages_fr.properties @@ -82,4 +82,6 @@ licence.membre.n.2.inconnue=Licence du membre n licence.membre.n.3.inconnue=Licence du membre n°3 inconnue demande.d.affiliation.non.trouve=Demande d'affiliation introuvable carton.non.trouver=Carton introuvable -card.cannot.be.added=Impossible d'ajouter le carton \ No newline at end of file +card.cannot.be.added=Impossible d'ajouter le carton +configuration.non.supportee=Configuration non supportée +err.match.termine=Erreur, un match de classement a déjà été joué \ No newline at end of file diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js index 8fbc892..9b2b7c1 100644 --- a/src/main/webapp/public/competition.js +++ b/src/main/webapp/public/competition.js @@ -104,8 +104,13 @@ function scorePrint(s1) { return String(s1); } } + function scoreToString(score) { - return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); + if (score.length === 0) + return ""; + if (score.at(0) instanceof Array) + return score.map(s => scorePrint(s.at(0)) + "-" + scorePrint(s.at(1))).join(" | "); + return score.map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | "); } function dateToString(date) { @@ -129,7 +134,7 @@ function dateToString(date) { return date_.toLocaleDateString(); } -function buildPouleMenu(isPoule, change_view) { +function buildPouleMenu(isPoule, change_view, isClassement = false) { const menuDiv = document.createElement('div'); menuDiv.id = 'menu'; menuDiv.style.borderBottom = '1px solid #9EA0A1'; @@ -188,7 +193,7 @@ function buildPouleMenu(isPoule, change_view) { change_view(true); }); ul.appendChild(li1); - const li2 = createTab(i18next.t('tournois'), !isPoule, function () { + const li2 = createTab(isClassement ? i18next.t('classement') : i18next.t('tournois'), !isPoule, function () { change_view(false); }); ul.appendChild(li2); @@ -262,7 +267,7 @@ function buildRankArray(rankArray) { } function buildTree(treeData) { - return drawGraph(initTree(treeData)) + return drawGraph(initTree(treeData.filter(d => d.data.level >= 0))) } function poulePage(location) { @@ -310,7 +315,7 @@ function poulePage(location) { dataContainer.append(buildTree(poule['trees'])); } else { const change_view = (isPoule) => { - dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view)); + dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view, poule['treeIsClassement'])); if (isPoule) { for (const g in poule.matchs) { @@ -325,6 +330,19 @@ function poulePage(location) { } } else { dataContainer.append(buildTree(poule['trees'])); + if (poule['treeIsClassement'] && poule['trees'].some(d => d.data.level <= -10)) { + dataContainer.append(buildMatchArray( + poule['trees'].filter(d => d.data.level < 0).reverse().map(d => ({ + red: d.data.c1FullName, + blue: d.data.c2FullName, + score: d.data.scores, + end: d.data.end, + red_w: d.data.win > 0, + blue_w: d.data.win < 0, + eq: d.data.win === 0, + date: d.data.date, + })))); + } } location[2] = isPoule ? 1 : 2; @@ -392,10 +410,10 @@ function buildCombView(comb) {

${i18next.t('statistique')} :

  • ${i18next.t('tauxDeVictoire2', { - nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.filter(m => m.end).length * 100).toFixed(0), - victoires: comb.totalWin, - matchs: comb.matchs.filter(m => m.end).length - })} + nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.filter(m => m.end).length * 100).toFixed(0), + victoires: comb.totalWin, + matchs: comb.matchs.filter(m => m.end).length + })}
  • ${i18next.t('pointsMarqués2', {nb: comb.pointMake})}
  • ${i18next.t('pointsReçus2', {nb: comb.pointTake})}
  • diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index 0ac10d4..c929839 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -35,6 +35,7 @@ "chrono.entrezLeTempsEnS": "Enter time in seconds", "chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}", "chronomètre": "Stopwatch", + "classement": "Ranking", "club": "Club", "compétition": "Competition", "compétitionManager": "Competition manager", @@ -55,6 +56,8 @@ "conserverUniquementLesMatchsTerminés": "Keep only finished matches", "contre": "vs", "couleur": "Color", + "créerLesMatchesDeClassement": "Create the ranking matches", + "créerLesMatchesDeClassement.msg": "Ranking matches have already been set up/played; recreating these matches will delete them all (you will therefore lose any results). Please note down any information you wish to keep.", "créerLesMatchs": "Create matches", "date": "Date", "demi-finalesEtFinales": "Semi-finals and finals", @@ -80,6 +83,8 @@ "genre.na": "NA", "individuelle": "Individual", "inscrit": "Registered", + "leTournoiServiraDePhaseFinaleAuxPoules": "The tournament will serve as the final phase for the group stage.", + "lesCombattantsEnDehors": "Fighters not participating in the tournament will have a ranking match.", "listeDesCartons": "List of cards", "manche": "Round", "matchPourLesPerdantsDuTournoi": "Match for tournament losers:", @@ -134,6 +139,9 @@ "toast.deleteCategory.error": "Error while deleting the category", "toast.deleteCategory.pending": "Deleting category...", "toast.deleteCategory.success": "Category deleted!", + "toast.matchs.classement.create.error": "Error while creating ranking matches.", + "toast.matchs.classement.create.pending": "Creating ranking matches in progress...", + "toast.matchs.classement.create.success": "Ranking matches created successfully.", "toast.matchs.create.error": "Error while creating matches.", "toast.matchs.create.pending": "Creating matches in progress...", "toast.matchs.create.success": "Matches created successfully.", diff --git a/src/main/webapp/public/locales/en/result.json b/src/main/webapp/public/locales/en/result.json index c43e1e5..5448a44 100644 --- a/src/main/webapp/public/locales/en/result.json +++ b/src/main/webapp/public/locales/en/result.json @@ -10,6 +10,7 @@ "bleu": "Blue", "catégorie": "Category", "chargement": "Loading", + "classement": "Ranking", "club": "Club", "combattant": "Fighter", "combattants": "Fighters", @@ -64,5 +65,6 @@ "tournois": "Tournaments", "tousLesCombattants": "All fighters", "victoire": "Win", - "victoires": "Wins" + "victoires": "Wins", + "classementFinal": "Final standings" } diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 5ed39bf..a23bd18 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -35,6 +35,7 @@ "chrono.entrezLeTempsEnS": "Entrez le temps en s", "chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}", "chronomètre": "Chronomètre", + "classement": "Classement", "club": "Club", "compétition": "Compétition", "compétitionManager": "Compétition manager", @@ -55,6 +56,8 @@ "conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés", "contre": "contre", "couleur": "Couleur", + "créerLesMatchesDeClassement": "Créer les matches de classement", + "créerLesMatchesDeClassement.msg": "Des matches de classement ont déjà été configurer/jouer, la recréation de ces matches vont tous les supprimer (vous perdre donc les résultats s'il y en a). Mercie de noter de votre côté les informations que vous voulez conserver.", "créerLesMatchs": "Créer les matchs", "date": "Date", "demi-finalesEtFinales": "Demi-finales et finales", @@ -80,6 +83,8 @@ "genre.na": "NA", "individuelle": "Individuelle", "inscrit": "Inscrit", + "leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules", + "lesCombattantsEnDehors": "Les combattants en dehors du tournoi auront un match de classement", "listeDesCartons": "Liste des cartons", "manche": "Manche", "matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:", @@ -134,6 +139,9 @@ "toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie", "toast.deleteCategory.pending": "Suppression de la catégorie...", "toast.deleteCategory.success": "Catégorie supprimée !", + "toast.matchs.classement.create.error": "Erreur lors de la création des matchs de classement.", + "toast.matchs.classement.create.pending": "Création des matchs de classement en cours...", + "toast.matchs.classement.create.success": "Matchs de classement créés avec succès.", "toast.matchs.create.error": "Erreur lors de la création des matchs.", "toast.matchs.create.pending": "Création des matchs en cours...", "toast.matchs.create.success": "Matchs créés avec succès.", diff --git a/src/main/webapp/public/locales/fr/result.json b/src/main/webapp/public/locales/fr/result.json index ec2c1ec..958a9ab 100644 --- a/src/main/webapp/public/locales/fr/result.json +++ b/src/main/webapp/public/locales/fr/result.json @@ -10,6 +10,7 @@ "bleu": "Bleu", "catégorie": "Catégorie", "chargement": "Chargement", + "classement": "Classement", "club": "Club", "combattant": "Combattant", "combattants": "Combattants", @@ -64,5 +65,6 @@ "tournois": "Tournois", "tousLesCombattants": "Tous les combattants", "victoire": "Victoire", - "victoires": "Victoires" + "victoires": "Victoires", + "classementFinal": "Classement final" } diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 0336396..14ed64e 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -18,7 +18,7 @@ import {getToastMessage} from "../../../utils/Tools.js"; import {copyStyles} from "../../../utils/copyStyles.js"; import {StateWindow} from "./StateWindow.jsx"; import {CombName, useCombs} from "../../../hooks/useComb.jsx"; -import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; +import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -36,7 +36,10 @@ export function CMAdmin({compUuid}) { ...cat_, name: data.name, liceName: data.liceName, - type: data.type + type: data.type, + treeAreClassement: data.treeAreClassement, + fullClassement: data.fullClassement, + preset: data.preset, })) } dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) @@ -465,7 +468,7 @@ function TeamCardModal() { {card.teamName} : <> - {card.teamCard ? "|-> " + t('team'): t('individuelle')} + {card.teamCard ? "|-> " + t('team') : t('individuelle')} } @@ -569,7 +572,7 @@ function CategoryHeader({
    {cat && -
    Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? t('tournois') : ""} | +
    Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? (cat.treeAreClassement ? t('classement') : t('tournois')) : ""} | Zone: {cat.liceName}
    }
    @@ -603,6 +606,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { const [lice, setLice] = useState("1") const [poule, setPoule] = useState(true) const [tournoi, setTournoi] = useState(false) + const [classement, setClassement] = useState(true) + const [fullClassement, setFullClassement] = useState(false) const [size, setSize] = useState(4) const [loserMatch, setLoserMatch] = useState(1) const {t} = useTranslation("cm"); @@ -614,14 +619,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { setLice(state.liceName || "1"); setPoule(((state.type || 1) & 1) !== 0); setTournoi((state.type & 2) !== 0); + setClassement(state.treeAreClassement !== undefined && state.treeAreClassement !== false); + setFullClassement(state.fullClassement !== undefined && state.fullClassement !== false); - if (state?.trees && state.trees.length >= 1) { - const tree = state.trees[0]; + const trees_ = state?.trees ? state.trees.filter(t => t.level > 0) : null; + if (trees_ && trees_.length >= 1) { + const tree = trees_[0]; setSize(tree.getMaxChildrenAtDepth(tree.death() - 1) * 2); - if (state.trees.length === 1) { + if (trees_.length === 1) { setLoserMatch(0); - } else if (state.trees.length === 2) { + } else if (trees_.length === 2) { setLoserMatch(1); } else { setLoserMatch(-1); @@ -647,24 +655,27 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { return; } + const trees_ = state?.trees ? state.trees.filter(t => t.level > 0) : null; if (state?.id) { const applyChanges = () => { const newData = { id: state.id, name: name.trim(), liceName: lice.trim(), - type: nType + type: nType, + treeAreClassement: classement, + fullClassement: fullClassement } let nbMatch = -1; let oldSubTree = -1; - const oldTrees = state?.trees || []; + const oldTrees = trees_ || []; if (oldTrees.length >= 1) { - const tree = state.trees[0]; + const tree = trees_[0]; nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1); - if (state.trees.length === 1) + if (trees_.length === 1) oldSubTree = 0 - else if (state.trees.length === 2) + else if (trees_.length === 2) oldSubTree = 1 } @@ -738,15 +749,6 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { } }) } - - const data = { - name: name.trim(), - liceName: lice.trim(), - type: poule + (tournoi << 1), - size: size, - loserMatch: loserMatch - } - console.log("Submitting category data:", data); } return
    @@ -782,8 +784,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { onChange={e => setTournoi(e.target.checked)}/>
    + +
    + setClassement(e.target.checked)}/> + +
    +
    +
    + setFullClassement(e.target.checked)}/> + +
    +
    diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 1e6aebd..d879ece 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -6,12 +6,13 @@ import {DrawGraph} from "../../result/DrawGraph.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {MarchReducer} from "../../../utils/MatchReducer.jsx"; -import {virtualScore, win_end} from "../../../utils/Tools.js"; +import {getToastMessage, virtualScore, win_end} from "../../../utils/Tools.js"; import "./CMTMatchPanel.css" import {useOBS} from "../../../hooks/useOBS.jsx"; import {useTranslation} from "react-i18next"; import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; import {ScorePanel} from "./ScoreAndCardPanel.jsx"; +import {toast} from "react-toastify"; function CupImg() { return { - setTrees(data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))) + setTrees({ + raw: data.trees.sort((a, b) => a.level - b.level), + formatted: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)) + }) cardDispatch({type: 'SET_ALL', payload: data.cards}); @@ -105,7 +109,10 @@ function CMTMatchPanel({catId, cat, menuActions}) { const treeListener = ({data}) => { if (data.length < 1 || data[0].categorie !== catId) return - setTrees(data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))) + setTrees({ + raw: data.sort((a, b) => a.level - b.level), + formatted: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)) + }) let matches2 = []; let combsToAdd = []; @@ -147,6 +154,7 @@ function CMTMatchPanel({catId, cat, menuActions}) { function ListMatch({cat, matches, trees, menuActions}) { const [type, setType] = useState(1); + const {sendRequest} = useWS(); const {t} = useTranslation("cm"); useEffect(() => { @@ -156,6 +164,10 @@ function ListMatch({cat, matches, trees, menuActions}) { setType(cat.type); }, [cat]); + const handleCreatClassement = () => { + toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm")) + } + if (!cat) return <>; @@ -169,7 +181,7 @@ function ListMatch({cat, matches, trees, menuActions}) {
  • setType(2)}>{t('tournois')} + onClick={_ => setType(2)}>{(cat.treeAreClassement ? t('classement') : t('tournois'))}
@@ -181,12 +193,14 @@ function ListMatch({cat, matches, trees, menuActions}) { } {type === 2 && <> - + {cat.treeAreClassement && !matches.some(m => m.categorie === cat.id && m.categorie_ord === -42 && (m.c1 !== undefined || m.c2 !== undefined)) ? <> + + : } } } -function MatchList({matches, cat, menuActions}) { +function MatchList({matches, cat, menuActions, classement = false, currentMatch = null, setCurrentMatch, getNext}) { const [activeMatch, setActiveMatch] = useState(null) const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1") const publicAffDispatch = usePubAffDispatch(); @@ -195,12 +209,17 @@ function MatchList({matches, cat, menuActions}) { const {sendNotify, setState} = useWS(); const liceName = (cat.liceName || "N/A").split(";"); - const marches2 = matches.filter(m => m.categorie_ord !== -42) - .sort((a, b) => a.categorie_ord - b.categorie_ord) - .map(m => ({...m, ...win_end(m, cards_v)})) + const marches2 = classement + ? matches.filter(m => m.categorie_ord === -42) + .map(m => ({...m, ...win_end(m, cards_v)})) + : matches.filter(m => m.categorie_ord !== -42) + .sort((a, b) => a.categorie_ord - b.categorie_ord) + .map(m => ({...m, ...win_end(m, cards_v)})) const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; const isActiveMatch = (index) => { + if (classement) + return true; return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice) } @@ -225,16 +244,26 @@ function MatchList({matches, cat, menuActions}) { useEffect(() => { if (match && match.poule !== lice) - setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id) + if (!classement) + setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id) }, [lice]); useEffect(() => { if (marches2.length === 0) return; - if (marches2.some(m => m.id === activeMatch)) + if (marches2.some(m => m.id === (classement ? currentMatch?.matchSelect : activeMatch))) return; - setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id); + if (classement) { + getNext.current = (id) => { + if (id === null) + return marches2.findLast((m, index) => !m.end && isActiveMatch(index))?.id; + const index = marches2.findIndex(m => m.id === id); + return marches2.slice(0, index).reverse().find((m, index2) => !m.end && isActiveMatch(marches2.length - 1 - index2))?.id; + } + } else { + setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id); + } }, [matches]) useEffect(() => { @@ -242,6 +271,13 @@ function MatchList({matches, cat, menuActions}) { sendNotify("sendSelectMatch", activeMatch); }, [activeMatch]); + const handleMatchClick = (matchId) => { + if (classement) { + setCurrentMatch({matchSelect: matchId, matchNext: marches2.reverse().find(m => !m.end && m.id !== matchId)?.id}); + } else { + setActiveMatch(matchId); + } + } const GetCard = ({combId, match, cat}) => { const c = getHeightCardForCombInMatch(combId, match) @@ -287,8 +323,8 @@ function MatchList({matches, cat, menuActions}) { - - + {!classement && } + {!classement && } @@ -299,11 +335,11 @@ function MatchList({matches, cat, menuActions}) { {marches2.map((m, index) => ( setActiveMatch(m.id)}> - - + className={m.id === (classement ? currentMatch?.matchSelect : activeMatch) ? "table-primary" : (m.end ? "table-success" : (isActiveMatch(index) ? "" : "table-warning"))} + onClick={() => handleMatchClick(m.id)}> + {!classement && } + {!classement && } @@ -320,18 +356,20 @@ function MatchList({matches, cat, menuActions}) {
ZPZP{t('no')} {t('rouge')}
- {liceName[(index - firstIndex) % liceName.length]}{m.poule} + {liceName[(index - firstIndex) % liceName.length]}{m.poule} {index >= firstIndex ? index + 1 - firstIndex : ""} {m.end && m.win > 0 && }
- {activeMatch && + {activeMatch && !classement && } } -function BuildTree({treeData, matches, menuActions}) { +function BuildTree({treeData, matches, cat, menuActions}) { const scrollRef = useRef(null) const [currentMatch, setCurrentMatch] = useState(null) const {getComb} = useCombs() const publicAffDispatch = usePubAffDispatch(); const {cards_v} = useCards(); const {sendNotify, setState} = useWS(); + const getNext = useRef(null); + const rtrees = useRef(null); const match = matches.find(m => m.id === currentMatch?.matchSelect) useEffect(() => { @@ -350,11 +388,11 @@ function BuildTree({treeData, matches, menuActions}) { } }, [next_match]); - function parseTree(data_in) { + function parseTree(data_in, matches_) { if (data_in?.data == null) return null - const matchData = matches.find(m => m.id === data_in.data) + const matchData = matches_.find(m => m.id === data_in.data) const c1 = getComb(matchData?.c1) const c2 = getComb(matchData?.c2) @@ -375,24 +413,57 @@ function BuildTree({treeData, matches, menuActions}) { c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null, c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null }) - node.left = parseTree(data_in?.left) - node.right = parseTree(data_in?.right) + node.left = parseTree(data_in?.left, matches_) + node.right = parseTree(data_in?.right, matches_) return node } - function initTree(data_in) { + function initTree(data_in, matches_) { let out = [] - for (const din of data_in) { - out.push(parseTree(din)) + let out2 = [] + for (let i = 0; i < data_in.raw.length; i++) { + if (data_in.raw.at(i).level > -10) { + out.push(parseTree(data_in.formatted.at(i), matches_)) + out2.push(parseTree(data_in.formatted.at(i), matches_)) + } } - return out + return [out, out2.reverse()] } - const trees = initTree(treeData); + const [trees, rTrees] = initTree(treeData, matches); + rtrees.current = rTrees; + + useEffect(() => { + if (matches.length === 0) + return; + if (matches.some(m => m.id === currentMatch?.matchSelect)) + return; + + const timeout = setTimeout(() => { + const rTrees_ = rtrees.current ? rtrees.current : rTrees; + const matchId = ((getNext.current) ? getNext.current(null) : null) || new TreeNode(null).nextMatchTree(rTrees_); + const next = matchId ? ((getNext.current) ? getNext.current(matchId) : null) || new TreeNode(matchId).nextMatchTree(rTrees_) : null; + setCurrentMatch({matchSelect: matchId, matchNext: next}); + setState({selectedMatch: matchId}); + sendNotify("sendSelectMatch", matchId); + }, 200); + return () => clearTimeout(timeout); + }, [matches]) + + const setCurrentMatch_ = (o) => { + const rTrees_ = rtrees.current ? rtrees.current : rTrees; + const matchId = o.matchSelect ? o.matchSelect : new TreeNode(null).nextMatchTree(rTrees_); + const next = o.matchNext ? o.matchNext : new TreeNode(matchId).nextMatchTree(rTrees_); + + setCurrentMatch({matchSelect: matchId, matchNext: next}); + setState({selectedMatch: matchId}); + sendNotify("sendSelectMatch", matchId); + } const onMatchClick = (rect, matchId, __) => { - setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())}); + const rTrees_ = rtrees.current ? rtrees.current : rTrees; + setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(rTrees_)}); setState({selectedMatch: matchId}); sendNotify("sendSelectMatch", matchId); } @@ -406,6 +477,10 @@ function BuildTree({treeData, matches, menuActions}) {
+ {cat.fullClassement && + n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))} + cat={cat} menuActions={menuActions} classement={true} currentMatch={currentMatch} setCurrentMatch={setCurrentMatch_} + getNext={getNext}/>}
diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 717510b..dcfd6ab 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -251,7 +251,7 @@ function Menu({menuActions}) { s + + + + } -function MatchList({matches, cat, groups, reducer}) { +function MatchList({matches, cat, groups, reducer, classement = false}) { const {sendRequest} = useWS(); const setLoading = useLoadingSwitcher() const selectRef = useRef(null) @@ -475,9 +501,13 @@ function MatchList({matches, cat, groups, reducer}) { const matchModal = useRef(null); const liceName = (cat.liceName || "N/A").split(";"); - const marches2 = matches.filter(m => m.categorie_ord !== -42) - .sort((a, b) => a.categorie_ord - b.categorie_ord) - .map(m => ({...m, ...win_end(m, cards_v)})) + const marches2 = classement + ? matches.filter(m => m.categorie_ord === -42) + .map(m => ({...m, ...win_end(m, cards_v)})) + : matches.filter(m => m.categorie_ord !== -42) + .sort((a, b) => a.categorie_ord - b.categorie_ord) + .map(m => ({...m, ...win_end(m, cards_v)})) + marches2.forEach(m => { if (m.end && (!m.scores || m.scores.length === 0)) m.scores = [{n_round: 0, s1: 0, s2: 0}]; @@ -491,6 +521,9 @@ function MatchList({matches, cat, groups, reducer}) { ); const handleDragEnd = async (event) => { + if (classement) + return; + const {active, over} = event; if (active.id !== over.id) { const newIndex = marches2.findIndex(m => m.id === over.id); @@ -570,7 +603,7 @@ function MatchList({matches, cat, groups, reducer}) { const handleDelMatch = (matchId) => { const match = matches.find(m => m.id === matchId) - if (!match) + if (!match || classement) return; if (!confirm(t('confirm1'))) @@ -585,7 +618,7 @@ function MatchList({matches, cat, groups, reducer}) { const match = matches.find(m => m.id === matchId) if (!match) return; - setModalMatchId (matchId); + setModalMatchId(matchId); matchModal.current.click(); } @@ -645,41 +678,46 @@ function MatchList({matches, cat, groups, reducer}) { {t('no')} - {t('poule')} - {t('zone')} + {!classement && + {t('poule')}} + {!classement && + {t('zone')}} {t('rouge')} {t('blue')} {t('résultat')} - - + {!classement && } + {!classement && } {marches2.map((m, index) => ( {index + 1} - {m.poule} - {liceName[index % liceName.length]} + {!classement && {m.poule}} + {!classement && {liceName[index % liceName.length]}} {m.end && m.win > 0 && } - + handleCombClick(e, m.id, m.c1)}> - + handleCombClick(e, m.id, m.c2)}> {m.end && m.win < 0 && } {scoreToString2(m, cards_v)} handleEditMatch(m.id)}> - handleDelMatch(m.id)}> - - ☰ + {!classement && + handleDelMatch(m.id)}> + } + ☰ ))} - + {!classement && - @@ -695,7 +733,7 @@ function MatchList({matches, cat, groups, reducer}) { - + } @@ -770,7 +808,7 @@ function SortableRow({id, children}) { ); } -function BuildTree({treeData, matches, groups}) { +function BuildTree({treeData, treeRaw, matches, cat, groups}) { const scrollRef = useRef(null) const selectRef = useRef(null) const lastMatchClick = useRef(null) @@ -811,10 +849,12 @@ function BuildTree({treeData, matches, groups}) { return node } - function initTree(data_in) { + function initTree(data_in, data_raw) { let out = [] - for (const din of data_in) { - out.push(parseTree(din)) + for (let i = 0; i < data_raw.length; i++) { + if (data_raw.at(i).level > -10) { + out.push(parseTree(data_in.at(i))) + } } return out } @@ -878,7 +918,10 @@ function BuildTree({treeData, matches, groups}) { const combsIDs = groups.map(m => m.id); return
- + + {cat.fullClassement && + n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))} + groups={groups} cat={cat} reducer={undefined} classement={true}/>} onChange(Number(e.target.value))}> + + {data.sort((a, b) => a.name.localeCompare(b.name)).map(club => ())} + +
+ : error + ? + : + } + +} + +function Def() { + const {t} = useTranslation(); + + return
+ + +
; +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 2ba76e1..1cdcf1e 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -19,6 +19,7 @@ import {copyStyles} from "../../../utils/copyStyles.js"; import {StateWindow} from "./StateWindow.jsx"; import {CombName, useCombs} from "../../../hooks/useComb.jsx"; import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; +import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -610,6 +611,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { const [fullClassement, setFullClassement] = useState(false) const [size, setSize] = useState(4) const [loserMatch, setLoserMatch] = useState(1) + const [preset, setPreset] = useState(-1) const {t} = useTranslation("cm"); const {sendRequest} = useWS(); @@ -621,6 +623,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { setTournoi((state.type & 2) !== 0); setClassement(state.treeAreClassement !== undefined && state.treeAreClassement !== false); setFullClassement(state.fullClassement !== undefined && state.fullClassement !== false); + setPreset(state.preset?.id || -1); let trees_ = [] for (let i = 0; i < state?.raw_trees?.length; i++) { @@ -674,7 +677,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { liceName: lice.trim(), type: nType, treeAreClassement: classement, - fullClassement: fullClassement + fullClassement: fullClassement, + preset: {id: preset !== -1 ? preset : null} } let nbMatch = -1; @@ -745,7 +749,10 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { toast.promise(sendRequest('createCategory', { name: name.trim(), liceName: lice.trim(), - type: nType + type: nType, + treeAreClassement: classement, + fullClassement: fullClassement, + preset: {id: preset !== -1 ? preset : null} }), getToastMessage("toast.createCategory", "cm") ).then(id => { if (tournoi) { @@ -774,6 +781,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { onChange={e => setName(e.target.value)}/> + +
- +
{cat && } @@ -177,7 +177,7 @@ export function CategoryContent({cat, catId, setCat, menuActions}) { } -function AddComb({groups, setGroups, removeGroup, menuActions}) { +function AddComb({groups, setGroups, removeGroup, menuActions, cat}) { const {data, setData} = useRequestWS("getRegister", null) const combDispatch = useCombsDispatch() const {dispatch} = useWS() @@ -243,7 +243,8 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) { diff --git a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx index 53fdb59..49cc6ef 100644 --- a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx @@ -1,10 +1,11 @@ import {useCountries} from "../../../hooks/useCountries.jsx"; -import {useEffect, useReducer, useRef, useState} from "react"; +import React, {useEffect, useReducer, useRef, useState} from "react"; import {CatList, getCatName, getToastMessage} from "../../../utils/Tools.js"; import {CombName} from "../../../hooks/useComb.jsx"; import {useWS} from "../../../hooks/useWS.jsx"; import {useTranslation} from "react-i18next"; import {toast} from "react-toastify"; +import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; function SelectReducer(state, action) { switch (action.type) { @@ -59,7 +60,7 @@ function SelectReducer(state, action) { } } -export function SelectCombModalContent({data, groups, setGroups, teamMode = false}) { +export function SelectCombModalContent({data, groups, setGroups, teamMode = false, defaultPreset = -1}) { const country = useCountries('fr') const {t} = useTranslation("cm"); const {sendRequest, dispatch} = useWS() @@ -77,6 +78,11 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals const [weightMax, setWeightMax] = useState(0) const [team, setTeam] = useState(false) const [teamName, setTeamName] = useState(""); + const [preset, setPreset] = useState(-1) + + useEffect(() => { + setPreset(defaultPreset) + }, [defaultPreset]) const handleSubmit = (e) => { e.preventDefault(); @@ -149,7 +155,8 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals && (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin) && (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax) && (teamMode && (comb.teamMembers == null || comb.teamMembers.length === 0) || !teamMode - && ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team))) { + && ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team)) + && (preset === -1 || comb.categoriesInscrites.includes(preset))) { dataOut[id] = dataIn[id]; } } @@ -215,6 +222,8 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals
+ +
setCountry_(e.target.value)}> + + {country && Object.keys(country).sort((a, b) => { + if (a < b) return -1 + if (a > b) return 1 + return 0 + }).map((key, _) => { + return () + })} + +
+ + +
+ +
+
+ +
+
+ setGender((prev) => { + return {...prev, H: e.target.checked} + })}/> + +
+
+ setGender((prev) => { + return {...prev, F: e.target.checked} + })}/> + +
+
+ setGender((prev) => { + return {...prev, NA: e.target.checked} + })}/> + +
+
+
+
+ +
+
+ setTeam(e.target.checked)}/> + +
+
+
+
+ +
+
setWeightMin(Number(e.target.value))}/>
+
{t('select.à')}
+
setWeightMax(Number(e.target.value))}/>
+
{t('select.msg1')}
+
+
+
+
+
+ + {CatList.map((cat_, index) => { + return
+
+ setCat_(e, index)}/> + +
+
+ })} +
+
+ + {dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')} {dispoFiltered.length > 10 && + {t('uneCatégorieNePeutContenirPlusDe10Combattants')}} + + +
+ + + +
+ +} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index eb878bc..8fe0aff 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -21,6 +21,7 @@ import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard. import {ScorePanel} from "./ScoreAndCardPanel.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; +import {AutoCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -240,6 +241,10 @@ function AddComb({groups, setGroups, removeGroup, menuActions, cat}) { disabled={data === null} onClick={() => setModalMode(true)}>{t('ajouterUneTeam')} + +