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/CombEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java index 8083b58..b34bc13 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.domain.entity; +import fr.titionfire.ffsaf.data.model.CatPresetModel; import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.RegisterModel; @@ -28,6 +29,7 @@ public class CombEntity { int overCategory; Integer weight; List teamMembers; + List categoriesInscrites; public static CombEntity fromModel(MembreModel model) { if (model == null) @@ -36,7 +38,7 @@ public class CombEntity { return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(), model.getClub() == null ? null : model.getClub().getClubId(), model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(), - 0, null, new ArrayList<>()); + 0, null, new ArrayList<>(), new ArrayList<>()); } @@ -47,7 +49,15 @@ public class CombEntity { return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null, model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight(), Stream.concat(model.getComb().stream().map(CombEntity::fromModel), - model.getGuest().stream().map(CombEntity::fromModel)).toList()); + model.getGuest().stream().map(CombEntity::fromModel)).toList(), + new ArrayList<>()); + } + + public CombEntity addCategoriesInscrites(List categoriesInscrites) { + if (categoriesInscrites == null) + return this; + this.categoriesInscrites = categoriesInscrites.stream().map(CatPresetModel::getId).toList(); + return this; } public static CombEntity fromModel(RegisterModel registerModel) { @@ -58,6 +68,7 @@ public class CombEntity { return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(), registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(), registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(), - model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight(), new ArrayList<>()); + model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight(), new ArrayList<>(), + new ArrayList<>()); } } 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/CardService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java index 56c330f..8a26d00 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java @@ -94,6 +94,38 @@ public class CardService { }); } + public Uni> addTeamCartToNewComb(Long combId, String teamUuid, String teamName, + CompetitionModel competition) { + return clubCardRepository.list("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)", + Sort.ascending("type"), competition.getId(), teamUuid, teamName) + .chain(clubCards -> { + Uni queue = Uni.createFrom().voidItem(); + List addCards = new ArrayList<>(); + for (ClubCardModel clubCard : clubCards) { + CardModel model = new CardModel(); + model.setCompetition(competition); + model.setCompetitionId(competition.getId()); + model.setComb(combId); + model.setTeamCard(true); + model.setType(clubCard.getType()); + model.setDate(clubCard.getDate()); + + queue = queue.call(__ -> Panache.withTransaction(() -> cardRepository.persist(model)) + .invoke(addCards::add) + .call(() -> { + clubCard.getCardIds().add(model.getId()); + return Panache.withTransaction(() -> clubCardRepository.persist(clubCard)); + })); + } + return queue.replaceWith(addCards); + }); + } + + public Uni rmTeamCardFromComb(Long combId, String uuid) { + return cardRepository.delete("comb = ?1 AND competition.uuid = ?2 AND teamCard = True", combId, uuid) + .replaceWithVoid(); + } + public Uni> addTeamCard(CompetitionModel competition, String teamUuid, String teamName, CardModel.CardType type, String reason) { return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)", 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/PresetData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java index 60ec743..1993cda 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java @@ -4,10 +4,12 @@ import fr.titionfire.ffsaf.data.model.CatPresetModel; import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @Data +@NoArgsConstructor @AllArgsConstructor @RegisterForReflection public class PresetData { 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..8a1e528 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)); })); } @@ -97,13 +97,14 @@ public class RCard { @WSReceiver(code = "sendCardRm", permission = PermLevel.ADMIN) public Uni sendCardRm(WebSocketConnection connection, SendCardAdd card) { - return getById(card.matchId(), connection) - .chain(matchModel -> cardRepository.find("match = ?1 AND comb = ?2 AND type = ?3", - matchModel.getId(), card.combId(), card.type()) + return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult() + .chain(competition -> cardRepository.find( + "match IS NULL AND comb = ?1 AND type = ?2 AND competition = ?3 AND match " + (card.matchId() == null ? "IS NULL" : "= " + card.matchId()), + card.combId(), card.type(), competition) .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))) @@ -143,7 +144,7 @@ public class RCard { } @RegisterForReflection - public record SendCardAdd(long matchId, long combId, CardModel.CardType type, String reason) { + public record SendCardAdd(Long matchId, long combId, CardModel.CardType type, String reason) { } @RegisterForReflection 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..2f7dcf2 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,15 @@ public class RCategorie { @Inject CardService cardService; + @Inject + CardRepository cardRepository; + + @Inject + CatPresetRepository catPresetRepository; + + @Inject + ResultService resultService; + @Inject TradService trad; @@ -56,9 +68,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 +90,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) @@ -112,10 +127,21 @@ public class RCategorie { @WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN) public Uni updateCategory(WebSocketConnection connection, JustCategorie categorie) { return getById(categorie.id, connection) + .call(cat -> { + if (categorie.preset() == null) { + cat.setPreset(null); + return Uni.createFrom().item(cat); + } else { + return catPresetRepository.findById(categorie.preset().getId()) + .invoke(cat::setPreset); + } + }) .chain(cat -> { cat.setName(categorie.name); cat.setLiceName(categorie.liceName); cat.setType(categorie.type); + cat.setTreeAreClassement(categorie.treeAreClassement); + cat.setFullClassement(categorie.fullClassement); return Panache.withTransaction(() -> categoryRepository.persist(cat)); }) .call(cat -> { @@ -224,10 +250,155 @@ public class RCategorie { .replaceWithVoid(); } + @WSReceiver(code = "listPreset", permission = PermLevel.VIEW) + public Uni> listPreset(WebSocketConnection connection, Object o) { + return catPresetRepository.list("competition.uuid", connection.pathParam("uuid")) + .map(presets -> presets.stream().map(PresetData::fromModel).toList()); + } + + @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 +413,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/java/fr/titionfire/ffsaf/ws/recv/RRegister.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RRegister.java index c1f7f7c..29f2374 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RRegister.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RRegister.java @@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.ws.PermLevel; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -24,14 +25,20 @@ public class RRegister { @WSReceiver(code = "getRegister", permission = PermLevel.TABLE) public Uni> getRegister(WebSocketConnection connection, Object o) { + ArrayList combEntities = new ArrayList<>(); return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult() - .call(cm -> Mutiny.fetch(cm.getInsc())) - .call(cm -> Mutiny.fetch(cm.getGuests())) - .map(cm -> { - ArrayList combEntities = new ArrayList<>(); - combEntities.addAll(cm.getInsc().stream().map(CombEntity::fromModel).toList()); - combEntities.addAll(cm.getGuests().stream().map(CombEntity::fromModel).toList()); - return combEntities; - }); + .call(cm -> Mutiny.fetch(cm.getInsc()) + .onItem().transformToMulti(Multi.createFrom()::iterable) + .call(r -> Mutiny.fetch(r.getCategoriesInscrites())) + .map(r -> CombEntity.fromModel(r).addCategoriesInscrites(r.getCategoriesInscrites())) + .collect().asList() + .invoke(combEntities::addAll)) + .call(cm -> Mutiny.fetch(cm.getGuests()) + .onItem().transformToMulti(Multi.createFrom()::iterable) + .call(r -> Mutiny.fetch(r.getCategoriesInscrites())) + .map(r -> CombEntity.fromModel(r).addCategoriesInscrites(r.getCategoriesInscrites())) + .collect().asList() + .invoke(combEntities::addAll)) + .replaceWith(combEntities); } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SRegister.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SRegister.java index 9197d2e..9dd8384 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/send/SRegister.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SRegister.java @@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.ws.send; import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; import fr.titionfire.ffsaf.data.model.RegisterModel; import fr.titionfire.ffsaf.domain.entity.CombEntity; +import fr.titionfire.ffsaf.domain.service.CardService; import fr.titionfire.ffsaf.net2.MessageType; import fr.titionfire.ffsaf.ws.CompetitionWS; import fr.titionfire.ffsaf.ws.MessageOut; @@ -13,6 +14,7 @@ import io.quarkus.websockets.next.UserData; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.hibernate.reactive.mutiny.Mutiny; import java.util.List; import java.util.UUID; @@ -25,16 +27,30 @@ public class SRegister { @Inject OpenConnections connections; + @Inject + CardService cardService; + public Uni sendRegister(String uuid, RegisterModel registerModel) { - return send(uuid, "sendRegister", CombEntity.fromModel(registerModel)); + return Mutiny.fetch(registerModel.getCategoriesInscrites()).chain(o -> + send(uuid, "sendRegister", CombEntity.fromModel(registerModel).addCategoriesInscrites(o)) + .call(__ -> registerModel.getClub2() == null ? Uni.createFrom().voidItem() : + cardService.addTeamCartToNewComb(registerModel.getMembre().getId(), + registerModel.getClub2().getClubId(), registerModel.getClub2().getName(), + registerModel.getCompetition()) + .chain(cardModels -> send(uuid, "sendCards", cardModels)))); } public Uni sendRegister(String uuid, CompetitionGuestModel model) { - return send(uuid, "sendRegister", CombEntity.fromModel(model)); + return Mutiny.fetch(model.getCategoriesInscrites()).chain(o -> + send(uuid, "sendRegister", CombEntity.fromModel(model).addCategoriesInscrites(o)) + .call(__ -> cardService.addTeamCartToNewComb(model.getId() * -1, + null, model.getClub(), model.getCompetition()) + .chain(cardModels -> send(uuid, "sendCards", cardModels)))); } public Uni sendRegisterRemove(String uuid, Long combId) { - return send(uuid, "sendRegisterRemove", combId); + return send(uuid, "sendRegisterRemove", combId) + .call(__ -> cardService.rmTeamCardFromComb(combId, uuid)); } public Uni send(String uuid, String code, Object data) { 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..28cde4b 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,36 @@ function buildRankArray(rankArray) { } function buildTree(treeData) { - return drawGraph(initTree(treeData)) + return drawGraph(initTree(treeData.filter(d => d.data.level >= 0))) +} + +function buildClassementArray(classement) { + const classement2 = classement.sort((a, b) => { + if (a.rank === b.rank) + return a.name.localeCompare(b.name); + return a.rank - b.rank; + }) + + const arrayDiv = document.createElement('div'); + let arrayContent = `
+ + + + + + + ` + for (const row of classement2) { + arrayContent += ` + + + + ` + + } + arrayContent += `
${i18next.t('place')}${i18next.t('nom')}
${row.rank}${row.name}
` + arrayDiv.innerHTML = arrayContent; + return arrayDiv; } function poulePage(location) { @@ -310,7 +344,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 +359,22 @@ 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, + })))); + } + if (poule['treeIsClassement']){ + dataContainer.append(buildClassementArray(poule['classement'])); + } } location[2] = isPoule ? 1 : 2; @@ -392,10 +442,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..5ccf2bc 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -5,6 +5,7 @@ "actuel": "Current", "administration": "Administration", "adresseDuServeur": "Server address", + "ajoutAutomatique": "Automatic addition", "ajouter": "Add", "ajouterDesCombattants": "Add fighters", "ajouterUn": "Add one", @@ -35,7 +36,9 @@ "chrono.entrezLeTempsEnS": "Enter time in seconds", "chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}", "chronomètre": "Stopwatch", + "classement": "Ranking", "club": "Club", + "combattantsCorrespondentAuxSélectionnés": "fighter(s) match the selections above", "compétition": "Competition", "compétitionManager": "Competition manager", "config.obs.dossierDesResources": "Resources folder", @@ -55,6 +58,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", @@ -79,10 +84,14 @@ "genre.h": "M", "genre.na": "NA", "individuelle": "Individual", + "informationCatégorie": "Category information", "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:", + "matchTerminé": "Match over", "matches": "Matches", "modifier": "Edit", "msg1": "There are already matches in this pool; what do you want to do with them?", @@ -92,6 +101,7 @@ "nomDeLaZone": "Area name", "nomDeLéquipe": "team name", "nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')", + "nombreDeCombattants": "Number of fighters", "nouvelle...": "New...", "obs.préfixDesSources": "Source prefix", "pays": "Country", @@ -99,6 +109,7 @@ "poule": "Pool", "poulePour": "Pool for: ", "préparation...": "Preparing...", + "remplacer": "Replace", "rouge": "Red", "réinitialiser": "Reset", "résultat": "Result", @@ -122,6 +133,7 @@ "supprimerUn": "Delete one", "sélectionneLesModesDaffichage": "Select display modes", "sélectionner": "Select", + "taille": "Size", "team": "Team", "terminé": "Finished", "texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.", @@ -134,6 +146,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.", @@ -167,6 +182,7 @@ "téléchargementEnCours": "Downloading...", "téléchargementTerminé!": "Download completed!", "uneCatégorie": "a category", + "uneCatégorieNePeutContenirPlusDe10Combattants": "A category cannot contain more than 10 fighters, please create weight categories.", "valider": "Validate", "zone": "Zone", "zoneDeCombat": "Combat zone" diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json index 33372fe..178174a 100644 --- a/src/main/webapp/public/locales/en/common.json +++ b/src/main/webapp/public/locales/en/common.json @@ -619,9 +619,9 @@ "supprimerLeCompte": "Delete account", "supprimerLeCompte.msg": "Are you sure you want to delete this account?", "sword.none": "$t(sans) / $t(nonDéfinie)", - "sword.oneHand": "One hand", + "sword.oneHand": "One hand sword", "sword.saber": "Saber", - "sword.twoHand": "Two hands", + "sword.twoHand": "Two hands sword", "sélectionEnéquipeDeFrance": "Selection in the French team", "sélectionner...": "Select...", "toast.edit.error": "Failed to save changes", diff --git a/src/main/webapp/public/locales/en/result.json b/src/main/webapp/public/locales/en/result.json index c43e1e5..4de482a 100644 --- a/src/main/webapp/public/locales/en/result.json +++ b/src/main/webapp/public/locales/en/result.json @@ -10,6 +10,8 @@ "bleu": "Blue", "catégorie": "Category", "chargement": "Loading", + "classement": "Ranking", + "classementFinal": "Final standings", "club": "Club", "combattant": "Fighter", "combattants": "Fighters", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 5ed39bf..384acfa 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -5,6 +5,7 @@ "actuel": "Actuel", "administration": "Administration", "adresseDuServeur": "Adresse du serveur", + "ajoutAutomatique": "Ajout automatique", "ajouter": "Ajouter", "ajouterDesCombattants": "Ajouter des combattants", "ajouterUn": "Ajouter un ", @@ -35,7 +36,9 @@ "chrono.entrezLeTempsEnS": "Entrez le temps en s", "chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}", "chronomètre": "Chronomètre", + "classement": "Classement", "club": "Club", + "combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélectionnés ci-dessus.", "compétition": "Compétition", "compétitionManager": "Compétition manager", "config.obs.dossierDesResources": "Dossier des resources", @@ -55,6 +58,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", @@ -79,10 +84,14 @@ "genre.h": "H", "genre.na": "NA", "individuelle": "Individuelle", + "informationCatégorie": "Information catégorie", "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:", + "matchTerminé": "Match terminé", "matches": "Matches", "modifier": "Modifier", "msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?", @@ -92,6 +101,7 @@ "nomDeLaZone": "Nom de la zone", "nomDeLéquipe": "Nom de l'équipe", "nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')", + "nombreDeCombattants": "Nombre de combattants", "nouvelle...": "Nouvelle...", "obs.préfixDesSources": "Préfix des sources", "pays": "Pays", @@ -99,6 +109,7 @@ "poule": "Poule", "poulePour": "Poule pour: ", "préparation...": "Préparation...", + "remplacer": "Remplacer", "rouge": "Rouge", "réinitialiser": "Réinitialiser", "résultat": "Résultat", @@ -122,6 +133,7 @@ "supprimerUn": "Supprimer un", "sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage", "sélectionner": "Sélectionner", + "taille": "Taille", "team": "Équipe", "terminé": "Terminé", "texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.", @@ -134,6 +146,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.", @@ -167,6 +182,7 @@ "téléchargementEnCours": "Téléchargement en cours...", "téléchargementTerminé!": "Téléchargement terminé !", "uneCatégorie": "une catégorie", + "uneCatégorieNePeutContenirPlusDe10Combattants": "Une catégorie ne peut contenir plus de 10 combattants, veuillez créer des catégories de poids.", "valider": "Valider", "zone": "Zone", "zoneDeCombat": "Zone de combat" diff --git a/src/main/webapp/public/locales/fr/common.json b/src/main/webapp/public/locales/fr/common.json index 790d076..7a41aa2 100644 --- a/src/main/webapp/public/locales/fr/common.json +++ b/src/main/webapp/public/locales/fr/common.json @@ -623,9 +623,9 @@ "supprimerLeCompte": "Supprimer le compte", "supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?", "sword.none": "$t(sans) / $t(nonDéfinie)", - "sword.oneHand": "Une main", + "sword.oneHand": "Épée une main", "sword.saber": "Sabre", - "sword.twoHand": "Deux mains", + "sword.twoHand": "Épée deux mains", "sélectionEnéquipeDeFrance": "Sélection en équipe de France", "sélectionner...": "Sélectionner...", "toast.edit.error": "Échec de l'enregistrement des modifications", diff --git a/src/main/webapp/public/locales/fr/result.json b/src/main/webapp/public/locales/fr/result.json index ec2c1ec..115350d 100644 --- a/src/main/webapp/public/locales/fr/result.json +++ b/src/main/webapp/public/locales/fr/result.json @@ -10,6 +10,8 @@ "bleu": "Bleu", "catégorie": "Catégorie", "chargement": "Chargement", + "classement": "Classement", + "classementFinal": "Classement final", "club": "Club", "combattant": "Combattant", "combattants": "Combattants", diff --git a/src/main/webapp/src/components/cm/AutoCatModalContent.jsx b/src/main/webapp/src/components/cm/AutoCatModalContent.jsx new file mode 100644 index 0000000..bd9e1ee --- /dev/null +++ b/src/main/webapp/src/components/cm/AutoCatModalContent.jsx @@ -0,0 +1,232 @@ +import React, {useEffect, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {useCountries} from "../../hooks/useCountries.jsx"; +import {ListPresetSelect} from "./ListPresetSelect.jsx"; +import {CatList, getCatName} from "../../utils/Tools.js"; + +export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) { + const country = useCountries('fr') + const {t} = useTranslation("cm"); + + const [country_, setCountry_] = useState("") + const [gender, setGender] = useState({H: true, F: true, NA: true}) + const [cat, setCat] = useState([]) + const [weightMin, setWeightMin] = useState(0) + const [weightMax, setWeightMax] = useState(0) + const [team, setTeam] = useState(false) + const [preset, setPreset] = useState(-1) + + useEffect(() => { + setPreset(defaultPreset) + }, [defaultPreset]) + + const setCat_ = (e, index) => { + if (e.target.checked) { + if (!cat.includes(index)) { + setCat([...cat, index]) + } + } else { + setCat(cat.filter(c => c !== index)) + } + } + + function applyFilter(dataIn, dataOut) { + dataIn.forEach(comb_ => { + const comb = data.find(d => d.id === comb_.id); + if (comb == null) + return; + if ((country_ === "" || comb.country === country_) + && (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA') + && (cat.includes(Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory))) + && (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin) + && (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax) + && ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team) + && (preset === -1 || comb.categoriesInscrites.includes(preset))) { + dataOut.push(comb) + } + } + ) + } + + const dispoFiltered = []; + if (data != null) + applyFilter(data, dispoFiltered); + + const makePoule = (combIn, groups) => { + combIn = combIn.sort(() => Math.random() - 0.5); + const maxInPoule = Math.ceil(combIn.length / 2); + const out = [] + + const pa = []; + const pb = []; + + let nameA; + let nameB; + groups.forEach(g => { + const existsInCombIn = combIn.some(c => c.id === g.id); + if (existsInCombIn) { + if ((pa.length === 0 || g.poule === nameA) && pa.length < maxInPoule) { + nameA = g.poule || "1"; + pa.push(g.id); + } else if ((pb.length === 0 || g.poule === nameB) && pb.length < maxInPoule) { + if (!(nameA === (g.poule || (nameA === "1" ? "2" : "1")))) { + nameB = g.poule || (nameA === "1" ? "2" : "1"); + pb.push(g.id); + } + } + } + }); + nameA = nameA || (nameB === "1" ? "2" : "1"); + nameB = nameB || (nameA === "1" ? "2" : "1"); + + if (combIn.length <= 5) { + combIn.forEach(c => { + if (!pa.includes(c.id)) + pa.push(c.id) + }); + } else { + for (const c of combIn) { + if (pa.includes(c.id) || pb.includes(c.id)) + continue; + + const club = c.club_str || (c.teamMembers && c.teamMembers[0].club_str) || ""; + + const countInPa = pa.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length; + const countInPb = pb.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length; + + if (pa.length < maxInPoule && (countInPa <= countInPb || pb.length >= maxInPoule)) { + pa.push(c.id); + } else if (pb.length < maxInPoule) { + pb.push(c.id); + } else { + pa.push(c.id); + } + } + } + + pa.forEach(id => out.push({id: id, poule: nameA})); + pb.forEach(id => out.push({id: id, poule: nameB})); + + return out + } + + const handleSubmit = (e) => { + e.preventDefault(); + + const toReplace = makePoule(dispoFiltered, groups); + setGroups(prev => [...prev.filter(g => !toReplace.some(r => r.id === g.id)), ...toReplace]); + } + + const handleReplace = (e) => { + e.preventDefault(); + + const toReplace = makePoule(dispoFiltered, []); + setGroups(prev => [...prev.map(g => ({id: g.id, poule: "-"})).filter(g => !toReplace.some(r => r.id === g.id)), ...toReplace]); + } + + return <> +
    +

    {t('ajoutAutomatique')}

    + +
    +
    +
    +
    + + +
    + + +
    + +
    +
    + +
    +
    + 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/components/cm/ListPresetSelect.jsx b/src/main/webapp/src/components/cm/ListPresetSelect.jsx new file mode 100644 index 0000000..eb7b371 --- /dev/null +++ b/src/main/webapp/src/components/cm/ListPresetSelect.jsx @@ -0,0 +1,37 @@ +import {useRequestWS} from "../../hooks/useWS.jsx"; +import {AxiosError} from "../AxiosError.jsx"; +import {useTranslation} from "react-i18next"; +import React, {useId} from "react"; + +export function ListPresetSelect({disabled, value, onChange}) { + const id = useId() + const {data, error} = useRequestWS("listPreset", {}, null); + const {t} = useTranslation(); + return <> + {data + ?
    + + +
    + : 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 0336396..1cdcf1e 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -18,7 +18,8 @@ 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"; +import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -36,7 +37,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 +469,7 @@ function TeamCardModal() { {card.teamName} : <> - {card.teamCard ? "|-> " + t('team'): t('individuelle')} + {card.teamCard ? "|-> " + t('team') : t('individuelle')} } @@ -569,7 +573,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,8 +607,11 @@ 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 [preset, setPreset] = useState(-1) const {t} = useTranslation("cm"); const {sendRequest} = useWS(); @@ -614,14 +621,23 @@ 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); + setPreset(state.preset?.id || -1); - if (state?.trees && state.trees.length >= 1) { - const tree = state.trees[0]; + let trees_ = [] + for (let i = 0; i < state?.raw_trees?.length; i++) { + if (state.raw_trees.at(i).level > 0) { + trees_.push(state.trees.at(i)) + } + } + 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 +663,33 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { return; } + let trees_ = [] + for (let i = 0; i < state?.raw_trees?.length; i++) { + if (state.raw_trees.at(i).level > 0) { + trees_.push(state.trees.at(i)) + } + } if (state?.id) { const applyChanges = () => { const newData = { id: state.id, name: name.trim(), liceName: lice.trim(), - type: nType + type: nType, + treeAreClassement: classement, + fullClassement: fullClassement, + preset: {id: preset !== -1 ? preset : null} } 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 } @@ -724,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) { @@ -738,15 +766,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
    @@ -762,6 +781,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { onChange={e => setName(e.target.value)}/>
    + +
    setTournoi(e.target.checked)}/>
    + +
    + setClassement(e.target.checked)}/> + +
    +
    +
    + setFullClassement(e.target.checked)}/> + +
    +
    diff --git a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx index 887d707..4dba14f 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx @@ -4,7 +4,7 @@ import {timePrint} from "../../../utils/Tools.js"; import {useTranslation} from "react-i18next"; import {useWS} from "../../../hooks/useWS.jsx"; -export function ChronoPanel() { +export function ChronoPanel({menuActions}) { const [config, setConfig] = useState({ time: Number(sessionStorage.getItem("chronoTime") || "90999"), pause: Number(sessionStorage.getItem("chronoPause") || "60999") @@ -25,6 +25,10 @@ export function ChronoPanel() { return chrono.time + Date.now() - chrono.startTime } + menuActions.current.setTimerConfig = (time, pause) => { + setConfig({time: time + 999, pause: pause + 999}) + } + useEffect(() => { publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: state.current.lastTimeStr, timeColor: state.current.color}}) }, []) diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 1e6aebd..293ba7c 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -6,12 +6,24 @@ 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 { + CatList, + getCatName, getShieldSize, + getShieldTypeName, getSwordSize, + getSwordTypeName, + getToastMessage, timePrint, + virtual_end, + 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"; +import {createPortal} from "react-dom"; +import ProtectionSelector from "../../../components/ProtectionSelector.jsx"; 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 +120,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 = []; @@ -142,11 +160,102 @@ function CMTMatchPanel({catId, cat, menuActions}) { } }, [catId]); - return + return <> + + + +} + +function SetTimeToChrono({cat, matches, menuActions}) { + const [catAverage, setCatAverage] = useState("---"); + const [genreAverage, setGenreAverage] = useState("H"); + const [nbComb, setNbComb] = useState(0); + const [preset, setPreset] = useState(undefined); + const [time, setTime] = useState({round: 0, pause: 0}); + const {cards_v} = useCards(); + + const {t} = useTranslation("cm"); + const {getComb} = useCombs(); + + useEffect(() => { + if (!cat || matches.filter(m => m.categorie === cat.id).length === 0) { + setCatAverage("---"); + setNbComb(0); + return; + } + setPreset(cat.preset); + + const genres = []; + const cats = []; + const combs = []; + for (const m of matches.filter(m => m.categorie === cat.id)) { + if (m.c1 && !combs.includes(m.c1)) + combs.push(m.c1); + if (m.c2 && !combs.includes(m.c2)) + combs.push(m.c2); + } + setNbComb(combs.length); + + combs.map(cId => getComb(cId, null)).filter(c => c && c.categorie) + .forEach(c => { + cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory)) + genres.push(c.genre) + }); + + const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length); + setCatAverage(CatList.at(catAvg) || "---"); + + const genreAvg = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length); + setGenreAverage(genreAvg > 0.5 ? "F" : "H"); + + if (!cat.preset || !cat.preset.categories) + return; + + const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie)); + + let p; + if (catAvailable.includes(catAvg)) { + p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg); + } else { + const closest = catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a); + p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === closest); + } + menuActions.current.setTimerConfig(p.roundDuration, p.pauseDuration) + setTime({round: p.roundDuration, pause: p.pauseDuration}) + + }, [cat, matches]); + + const marches2 = matches.filter(m => m.categorie === cat.id) + .map(m => ({...m, end: m.end || virtual_end(m, cards_v)})) + + return createPortal(
    +
    {t('informationCatégorie')}
    +
    +
    +
    +
    {t('catégorie')} : {getCatName(catAverage)}
    +
    {t('arme', {ns: 'common'})} : {getSwordTypeName(preset?.sword)} - {t('taille')} {getSwordSize(preset?.sword, catAverage, genreAverage)}
    +
    {t('bouclier', {ns: 'common'})} : {getShieldTypeName(preset?.shield)} - {t('taille')} {getShieldSize(preset?.shield, catAverage)}
    +
    {t('duréeRound')} : {timePrint(time.round)}
    +
    {t('duréePause')} : {timePrint(time.pause)}
    +
    {t('matchTerminé')}: {marches2.filter(m => m.end).length} sur {marches2.length}
    +
    {t('nombreDeCombattants')} : {nbComb}
    +
    +
    +
    {t('protectionObligatoire', {ns: 'common'})} :
    + { + }}/> +
    +
    +
    +
    , document.getElementById("infoCategory")) } function ListMatch({cat, matches, trees, menuActions}) { const [type, setType] = useState(1); + const {sendRequest} = useWS(); const {t} = useTranslation("cm"); useEffect(() => { @@ -156,6 +265,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 +282,7 @@ function ListMatch({cat, matches, trees, menuActions}) {
  • setType(2)}>{t('tournois')} + onClick={_ => setType(2)}>{(cat.treeAreClassement ? t('classement') : t('tournois'))}
@@ -181,12 +294,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 +310,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 +345,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 +372,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 +424,8 @@ function MatchList({matches, cat, menuActions}) { - - + {!classement && } + {!classement && } @@ -299,11 +436,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 +457,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 +489,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 +514,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 +578,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..f32af02 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -1,11 +1,11 @@ import React, {useEffect, useRef, useState} from "react"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; -import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; +import {useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {createPortal} from "react-dom"; import {copyStyles} from "../../../utils/copyStyles.js"; import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx"; -import {faArrowRightArrowLeft, faDisplay, faFile, faTrash} from "@fortawesome/free-solid-svg-icons"; +import {faArrowRightArrowLeft, faDisplay, faFile} from "@fortawesome/free-solid-svg-icons"; import {PubAffWindow} from "./PubAffWindow.jsx"; import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts"; import {ChronoPanel} from "./CMTChronoPanel.jsx"; @@ -15,7 +15,6 @@ import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import {Flip, toast} from "react-toastify"; import {useTranslation} from "react-i18next"; -import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; import {getToastMessage} from "../../../utils/Tools.js"; export function CMTable() { @@ -39,7 +38,7 @@ export function CMTable() {
{t('chronomètre')}
- +
@@ -49,9 +48,11 @@ export function CMTable() { + +
-
+
{t('matches')}
@@ -251,7 +252,7 @@ function Menu({menuActions}) { s + @@ -770,7 +822,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 +863,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 +932,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}/>}
+ +