diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java index 4868c3a..42f2a4a 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java @@ -10,6 +10,11 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + @Getter @Setter @AllArgsConstructor @@ -40,6 +45,22 @@ public class CompetitionGuestModel implements CombModel { Integer weight = null; + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + @JoinTable( + name = "groupe_membre", + joinColumns = @JoinColumn(name = "groupe_id"), + inverseJoinColumns = @JoinColumn(name = "membre_id") + ) + List comb = new ArrayList<>(); + + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + @JoinTable( + name = "groupe_guest", + joinColumns = @JoinColumn(name = "groupe_id"), + inverseJoinColumns = @JoinColumn(name = "guest_id") + ) + List guest = new ArrayList<>(); + public CompetitionGuestModel(String s) { this.fname = s.substring(0, s.indexOf(" ")); this.lname = s.substring(s.indexOf(" ") + 1); @@ -52,6 +73,8 @@ public class CompetitionGuestModel implements CombModel { @Override public String getName() { + if (this.isTeam()) + return this.fname; return this.fname + " " + this.lname; } @@ -59,4 +82,21 @@ public class CompetitionGuestModel implements CombModel { public String getName(MembreModel model, ResultPrivacy privacy) { return getName(); } + + public boolean isTeam() { + return "__team".equals(this.lname); + } + + public boolean isInTeam(Object comb_) { + if (!this.isTeam()) + return false; + + if (comb_ instanceof Long id_) { + if (id_ >= 0) + return comb.stream().anyMatch(membre -> Objects.equals(membre.getId(), id_)); + else + return guest.stream().anyMatch(guestModel -> Objects.equals(guestModel.getId(), -id_)); + } + return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_)); + } } 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 7fb7b9a..e017810 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -114,6 +114,9 @@ public class MatchModel { } public boolean isC1(Object comb) { + if (this.c1_guest != null && this.c1_guest.isInTeam(comb)) + return true; + if (comb instanceof Long id_) { if (id_ >= 0) return Objects.equals(this.c1_id != null ? this.c1_id.getId() : null, id_); @@ -124,6 +127,9 @@ public class MatchModel { } public boolean isC2(Object comb) { + if (this.c2_guest != null && this.c2_guest.isInTeam(comb)) + return true; + if (comb instanceof Long id_) { if (id_ >= 0) return Objects.equals(this.c2_id != null ? this.c2_id.getId() : null, id_); 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 4126db2..8083b58 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java @@ -9,6 +9,10 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + @Data @AllArgsConstructor @RegisterForReflection @@ -23,6 +27,7 @@ public class CombEntity { String country; int overCategory; Integer weight; + List teamMembers; public static CombEntity fromModel(MembreModel model) { if (model == null) @@ -31,7 +36,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); + 0, null, new ArrayList<>()); } @@ -40,7 +45,9 @@ public class CombEntity { return null; return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null, - model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight()); + model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight(), + Stream.concat(model.getComb().stream().map(CombEntity::fromModel), + model.getGuest().stream().map(CombEntity::fromModel)).toList()); } public static CombEntity fromModel(RegisterModel registerModel) { @@ -51,6 +58,6 @@ 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()); + model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight(), new ArrayList<>()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index a56026f..03faff3 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -325,7 +325,10 @@ public class CompetitionService { })) .chain(model -> { model.setFname(data.getFname()); - model.setLname(data.getLname()); + if (data.getLname().equals("__team")) + model.setLname("_team"); + else + model.setLname(data.getLname()); model.setGenre(data.getGenre()); model.setClub(data.getClub()); model.setCountry(data.getCountry()); @@ -467,7 +470,8 @@ public class CompetitionService { .call(cm -> membreService.getById(combId) .invoke(Unchecked.consumer(model -> { if (model == null) - throw new DNotFoundException(String.format(trad.t("le.membre.n.existe.pas"), combId)); + throw new DNotFoundException( + String.format(trad.t("le.membre.n.existe.pas"), combId)); if (!securityCtx.isInClubGroup(model.getClub().getId())) throw new DForbiddenException(); }))) 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 0156acb..1f61f08 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -291,7 +291,11 @@ public class ResultService { builder.nb_insc(combs.size()); builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); - builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); + builder.point(matchModels.stream() + .filter(MatchModel::isEnd) + .flatMap(m -> m.getScores().stream()) + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .mapToInt(s -> s.getS1() + s.getS2()).sum()); builder.combs(combs); return builder.build(); @@ -312,12 +316,12 @@ public class ResultService { .map(models -> { HashMap map = new HashMap<>(); models.forEach( - r -> map.put(Utils.getFullName(r.getMembre()), getCombTempId(r.getMembre().getId()))); + r -> map.put(r.getMembre().getName(), getCombTempId(r.getMembre().getId()))); return map; }) - .chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid) + .chain(map -> competitionGuestRepository.list("competition.uuid = ?1 AND lname != \"__team\"", uuid) .map(models -> { - models.forEach(guestModel -> map.put(Utils.getFullName(guestModel), + models.forEach(guestModel -> map.put(guestModel.getName(), getCombTempId(guestModel.getId() * -1))); return map; }) @@ -355,8 +359,10 @@ public class ResultService { builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2().getName(trad)); - return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid, - registerModel.getMembre()); + return matchRepository.list( + "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + + "WHERE m.category.compet.uuid = ?1 AND (m.c1_id = ?2 OR m.c2_id = ?2 OR c1g = ?2 OR c2g = ?2)", + uuid, registerModel.getMembre()); })); } else { uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult() @@ -366,7 +372,9 @@ public class ResultService { builder.cat( (guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad)); - return matchRepository.list("category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)", + return matchRepository.list( + "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + + "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)", uuid, guestModel); }); } @@ -389,12 +397,13 @@ public class ResultService { builder2.date(matchModel.getDate()); builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory())) .map(CategoryModel::getName).findFirst().orElse("")); + builder2.end(matchModel.isEnd()); AtomicInteger pointMake = new AtomicInteger(); AtomicInteger pointTake = new AtomicInteger(); if (matchModel.isC1(id)) { - builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest())); + builder2.adv(matchModel.getC2Name()); if (matchModel.isEnd()) { matchModel.getScores().stream() .filter(s -> s.getS1() > -900 && s.getS2() > -900) @@ -409,7 +418,7 @@ public class ResultService { } builder2.win(matchModel.isEnd() && matchModel.win() > 0); } else { - builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest())); + builder2.adv(matchModel.getC1Name()); if (matchModel.isEnd()) { matchModel.getScores().stream() .filter(s -> s.getS1() > -900 && s.getS2() > -900) @@ -468,7 +477,7 @@ public class ResultService { @Builder @RegisterForReflection public static record MatchsData(Date date, String poule, String adv, List score, float ratio, - boolean win, boolean eq) { + boolean win, boolean eq, boolean end) { } } @@ -513,14 +522,19 @@ public class ResultService { return Uni.createFrom().voidItem(); }) .chain(guests -> matchRepository.list( - "category.compet.uuid = ?1 AND (c1_guest IN ?2 OR c2_guest IN ?2)", uuid, guests) + "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + + "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)", + uuid, guests) .map(matchModels -> getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(), matchModels, new ArrayList<>(), membreModel))); } else { return clubRepository.findById(id).chain(clubModel -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .chain(registers -> matchRepository.list( + "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + + "WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)", + uuid, registers.stream().map(RegisterModel::getMembre).toList()) .map(matchModels -> getClubArray2(clubModel.getName(), registers.stream().map(o -> (CombModel) o.getMembre()).toList(), @@ -534,8 +548,8 @@ public class ResultService { builder.name(name); builder.nb_insc(combs.size()); - AtomicInteger tt_win = new AtomicInteger(0); - AtomicInteger tt_match = new AtomicInteger(0); + ArrayList win_ids = new ArrayList<>(); + ArrayList match_ids = new ArrayList<>(); List combData = combs.stream().map(comb -> { var builder2 = ClubArrayData.CombData.builder(); @@ -562,16 +576,16 @@ public class ResultService { builder2.pointTake(stat.pointTake); builder2.ratioPoint(stat.getPointRate()); - tt_win.addAndGet(stat.w); - tt_match.addAndGet(stat.w + stat.l); + win_ids.addAll(stat.win_ids); + match_ids.addAll(stat.match_ids); return builder2.build(); }) .sorted(Comparator.comparing(ClubArrayData.CombData::name)) .toList(); - builder.nb_match(tt_match.get()); - builder.match_w(tt_win.get()); + builder.nb_match((int) match_ids.stream().distinct().count()); + builder.match_w((int) win_ids.stream().distinct().count()); builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); @@ -588,11 +602,14 @@ public class ResultService { matchModels.stream() .filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb))) .forEach(matchModel -> { + stat.match_ids.add(matchModel.getId()); + int win = matchModel.win(); if (win == 0) { stat.score += 1; } else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) { stat.w++; + stat.win_ids.add(matchModel.getId()); stat.score += 3; } else { stat.l++; @@ -631,6 +648,8 @@ public class ResultService { public int score; public int pointMake; public int pointTake; + public ArrayList win_ids = new ArrayList<>(); + public ArrayList match_ids = new ArrayList<>(); public CombStat() { this.w = 0; diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index d5246f3..e346295 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -48,6 +48,9 @@ public class CompetitionWS { @Inject RCardboard rCardboard; + @Inject + RTeam rTeam; + @Inject SecurityCtx securityCtx; @@ -91,6 +94,7 @@ public class CompetitionWS { getWSReceiverMethods(RCategorie.class, rCategorie); getWSReceiverMethods(RRegister.class, rRegister); getWSReceiverMethods(RCardboard.class, rCardboard); + getWSReceiverMethods(RTeam.class, rTeam); executor = notifyExecutor; } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java new file mode 100644 index 0000000..54015fd --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java @@ -0,0 +1,137 @@ +package fr.titionfire.ffsaf.ws.recv; + +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; +import fr.titionfire.ffsaf.data.model.RegisterModel; +import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.data.repository.RegisterRepository; +import fr.titionfire.ffsaf.domain.entity.CombEntity; +import fr.titionfire.ffsaf.domain.service.TradService; +import fr.titionfire.ffsaf.utils.Categorie; +import fr.titionfire.ffsaf.utils.Genre; +import fr.titionfire.ffsaf.utils.Pair; +import fr.titionfire.ffsaf.ws.PermLevel; +import fr.titionfire.ffsaf.ws.send.SSRegister; +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.WebSocketConnection; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@WithSession +@ApplicationScoped +@RegisterForReflection +public class RTeam { + + @Inject + TradService trad; + + @Inject + CompetitionRepository competitionRepository; + + @Inject + RegisterRepository registerRepository; + + @Inject + CompetitionGuestRepository competitionGuestRepository; + + @WSReceiver(code = "setTeam", permission = PermLevel.ADMIN) + public Uni setTeam(WebSocketConnection connection, TeamData data) { + return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult() + .chain(cm -> registerRepository.list("membre.id IN ?1 AND competition = ?2", + data.members.stream().filter(id -> id >= 0).toList(), cm) + .chain(l -> competitionGuestRepository.list("id IN ?1", + data.members.stream().filter(id -> id < 0).map(i -> i * -1).toList()) + .map(l2 -> new Pair<>(l, l2))) + .chain(pair -> + competitionGuestRepository.find("fname = ?1 AND lname = ?2 AND competition = ?3", + data.name, "__team", cm).firstResult() + .chain(team -> { + if (pair.getKey().isEmpty() && pair.getValue().isEmpty()) { + if (team != null) { + CompetitionGuestModel finalTeam1 = team; + SSRegister.sendRegisterRemove(connection, finalTeam1.getId() * -1); + return Panache.withTransaction( + () -> competitionGuestRepository.delete(finalTeam1)) + .replaceWith((CombEntity) null); + } else + return Uni.createFrom().item((CombEntity) null); + } + + if (team == null) { + // Create new team + team = new CompetitionGuestModel(); + team.setFname(data.name); + team.setLname("__team"); + team.setCompetition(cm); + team.setClub("Team"); + team.setGenre(Genre.NA); + } else { + team.getComb().clear(); + team.getGuest().clear(); + } + + team.setCategorie(Stream.concat( + pair.getKey().stream().map(RegisterModel::getCategorie2), + pair.getValue().stream().map(CompetitionGuestModel::getCategorie)) + .map(Enum::ordinal) + .max(Integer::compareTo) + .map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1)); + + List s = Stream.concat( + pair.getKey().stream().map(RegisterModel::getWeight), + pair.getValue().stream().map(CompetitionGuestModel::getWeight)) + .filter(Objects::nonNull).toList(); + if (s.isEmpty()) { + team.setWeight(null); + } else if (s.size() == 1) { + team.setWeight(s.get(0)); + } else { + team.setWeight((int) s.stream().mapToInt(Integer::intValue) + .average() + .orElse(0)); + } + + team.setCountry(Stream.concat( + pair.getKey().stream().map(m -> m.getMembre().getCountry()), + pair.getValue().stream().map(CompetitionGuestModel::getCountry)) + .filter(Objects::nonNull) + .map(String::toUpperCase) + .collect(Collectors.groupingBy( + e -> e, // Classer par élément + Collectors.counting() // Compter les occurrences + )) + .entrySet() + .stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("FR")); + + team.getComb().addAll( + pair.getKey().stream().map(RegisterModel::getMembre).toList()); + team.getGuest().addAll(pair.getValue()); + + CompetitionGuestModel finalTeam = team; + return Panache.withTransaction( + () -> competitionGuestRepository.persistAndFlush(finalTeam)) + .map(CombEntity::fromModel); + })) + ) + .invoke(combEntity -> { + if (combEntity != null) + SSRegister.sendRegister(connection, combEntity); + }); + } + + @RegisterForReflection + public record TeamData(List members, String name) { + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSRegister.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSRegister.java new file mode 100644 index 0000000..e34eac1 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSRegister.java @@ -0,0 +1,16 @@ +package fr.titionfire.ffsaf.ws.send; + +import fr.titionfire.ffsaf.domain.entity.CombEntity; +import fr.titionfire.ffsaf.ws.CompetitionWS; +import io.quarkus.websockets.next.WebSocketConnection; + +public class SSRegister { + + public static void sendRegister(WebSocketConnection connection, CombEntity combEntity) { + CompetitionWS.sendNotifyToOtherEditor(connection, "sendRegister", combEntity); + } + + public static void sendRegisterRemove(WebSocketConnection connection, Long combId) { + CompetitionWS.sendNotifyToOtherEditor(connection, "sendRegisterRemove", combId); + } +} diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js index 6f14d9c..e593364 100644 --- a/src/main/webapp/public/competition.js +++ b/src/main/webapp/public/competition.js @@ -393,9 +393,9 @@ function buildCombView(comb) {

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

  • ${i18next.t('tauxDeVictoire2', { - nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0), + nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.filter(m => m.end).length * 100).toFixed(0), victoires: comb.totalWin, - matchs: comb.matchs.length + matchs: comb.matchs.filter(m => m.end).length })}
  • ${i18next.t('pointsMarqués2', {nb: comb.pointMake})}
  • @@ -423,7 +423,7 @@ function buildCombView(comb) { ${match.poule} ${match.adv} ${scoreToString(match.score)} - ${match.ratio.toFixed(3)} + ${match.end ? match.ratio.toFixed(3) : ""} ${match.win ? cupImg : (match.eq ? cupImg2 : "")} ` } diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index b21846e..5e0df67 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -1,11 +1,13 @@ { "--SélectionnerUnCombattant--": "-- Select a fighter --", "--Tous--": "-- All --", + "PourLéquipe": "for the team", "actuel": "Current", "administration": "Administration", "adresseDuServeur": "Server address", "ajouter": "Add", "ajouterDesCombattants": "Add fighters", + "ajouterUneTeam": "Add team", "attention": "Warning", "aucuneConfigurationObs": "No OBS configuration found, please import one", "bleu": "Blue", @@ -69,6 +71,7 @@ "neRienConserver": "Keep nothing", "no": "No.", "nom": "Name", + "nomDeLéquipe": "team name", "nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')", "nouvelle...": "New...", "obs.préfixDesSources": "Source prefix", @@ -122,8 +125,12 @@ "toast.updateTrees.init.success": "Trees created!", "toast.updateTrees.pending": "Updating tournament trees...", "toast.updateTrees.success": "Trees updated!", + "toast.team.update.error": "Error while updating team", + "toast.team.update.pending": "Updating team...", + "toast.team.update.success": "Team updated!", "tournoi": "Tournament", "tournois": "Tournaments", + "team": "Team", "tousLesMatchs": "All matches", "toutConserver": "Keep all", "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 6f620c6..4cc89af 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -1,11 +1,13 @@ { "--SélectionnerUnCombattant--": "-- Sélectionner un combattant --", "--Tous--": "-- Tous --", + "PourLéquipe": " pour l'équipe", "actuel": "Actuel", "administration": "Administration", "adresseDuServeur": "Adresse du serveur", "ajouter": "Ajouter", "ajouterDesCombattants": "Ajouter des combattants", + "ajouterUneTeam": "Ajouter une équipe", "attention": "Attention", "aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une", "bleu": "Bleu", @@ -69,6 +71,7 @@ "neRienConserver": "Ne rien conserver", "no": "N°", "nom": "Nom", + "nomDeLéquipe": "Nom de l'équipe", "nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')", "nouvelle...": "Nouvelle...", "obs.préfixDesSources": "Préfix des sources", @@ -122,8 +125,12 @@ "toast.updateTrees.init.success": "Arbres créés !", "toast.updateTrees.pending": "Mise à jour des arbres du tournoi...", "toast.updateTrees.success": "Arbres mis à jour !", + "toast.team.update.error": "Erreur lors de la mise à jour de l'équipe", + "toast.team.update.pending": "Mise à jour de l'équipe...", + "toast.team.update.success": "Équipe mise à jour !", "tournoi": "Tournoi", "tournois": "Tournois", + "team": "Équipe", "tousLesMatchs": "Tous les matchs", "toutConserver": "Tout conserver", "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", diff --git a/src/main/webapp/src/hooks/useComb.jsx b/src/main/webapp/src/hooks/useComb.jsx index aa62ac0..7ddf0a5 100644 --- a/src/main/webapp/src/hooks/useComb.jsx +++ b/src/main/webapp/src/hooks/useComb.jsx @@ -23,6 +23,7 @@ function reducer(state, action) { lname: action.payload.data.lname, genre: action.payload.data.genre, country: action.payload.data.country, + teamMembers: action.payload.data.teamMembers, }) if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) { //console.debug("Updating comb", comb); @@ -41,6 +42,7 @@ function reducer(state, action) { lname: e.lname, genre: e.genre, country: e.country, + teamMembers: e.teamMembers, } }); @@ -71,7 +73,7 @@ function WSListener({dispatch}) { useEffect(() => { const sendRegister = ({data}) => { - dispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); + dispatch({type: 'SET_COMB', payload: {source: "register", data: data}}); } dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}}) @@ -110,6 +112,8 @@ export function CombName({combId}) { const {getComb} = useCombs(); const comb = getComb(combId, null); if (comb) { + if (comb.lname === "__team") + return <>{comb.fname} return <>{comb.fname} {comb.lname} } else { return <>[Comb #{combId}] diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 9b5bcc3..a2e301b 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -500,15 +500,15 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { newTrees.push(trees2.at(i)); } - toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees") + toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees", "cm") ).then(__ => { - toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory")) + toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm")) }) } }) confirmRef.current.click(); } else { - toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory")) + toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm")) } } @@ -535,13 +535,13 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { name: name.trim(), liceName: lice.trim(), type: nType - }), getToastMessage("toast.createCategory") + }), getToastMessage("toast.createCategory", "cm") ).then(id => { if (tournoi) { const trees = build_tree(size, loserMatch) console.log("Creating trees for new category:", trees); - toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init") + toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init", "cm") ).finally(() => setCatId(id)) } else { setCatId(id); @@ -640,7 +640,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { title: t('confirm4.title'), message: t('confirm4.msg', {name: state.name}), confirm: () => { - toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory") + toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory", "cm") ).then(() => setCatId(null)); } }) diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index db2cc35..d2f4a71 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -403,7 +403,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { menuActions.current.saveScore = (scoreRed, scoreBlue) => { const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue}; - toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore")); + toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore", "cm")); } return () => menuActions.current.saveScore = undefined; }, [matchId]) diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 9840be8..bb4ad89 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -170,6 +170,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) { const combDispatch = useCombsDispatch() const {dispatch} = useWS() const [modalId, setModalId] = useState(null) + const [modalMode, setModalMode] = useState(false) const {t} = useTranslation("cm"); useEffect(() => { @@ -220,13 +221,17 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) { return <> + + @@ -247,10 +252,13 @@ function GroupsList({groups, setModalId}) { const groups2 = groups.map(g => { const comb = getComb(g.id); - return {...g, name: comb ? comb.fname + " " + comb.lname : ""}; + return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []}; }).sort((a, b) => { - if (a.poule !== b.poule) + if (a.poule !== b.poule) { + if (a.poule === '-') return 1; + if (b.poule === '-') return -1; return a.poule.localeCompare(b.poule); + } return a.name.localeCompare(b.name); }).reduce((acc, curr) => { const poule = curr.poule; @@ -264,12 +272,20 @@ function GroupsList({groups, setModalId}) { return <> {Object.keys(groups2).map((poule) => (
    -
    {poule !== '-' ? (t('poule') +" : " + poule) : t('sansPoule')}
    +
    {poule !== '-' ? (t('poule') + " : " + poule) : t('sansPoule')}
      {groups2[poule].map((comb) => (
    1. setModalId(comb.id)}> -
      +
      + + {comb.teamMembers.length > 0 && <> + {comb.teamMembers.map((m) => ( +
      + +
      ))} + } +
      {comb.poule}
    2. ) )} @@ -369,12 +385,12 @@ function ListMatch({cat, matches, groups, reducer}) { const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(cat, matchesToKeep, groups.filter(g => g.poule !== '-')) toast.promise(sendRequest("recalculateMatch", { - categorie: cat.id, - newMatch, - matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), - matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), - matchesToRemove: matchesToRemove.map(m => m.id) - }), getToastMessage("toast.matchs.create")) + categorie: cat.id, + newMatch, + matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), + matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), + matchesToRemove: matchesToRemove.map(m => m.id) + }), getToastMessage("toast.matchs.create", "ns")) .finally(() => { console.log("Finished creating matches"); }) diff --git a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx index a247106..53fdb59 100644 --- a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx @@ -1,9 +1,10 @@ import {useCountries} from "../../../hooks/useCountries.jsx"; -import {useEffect, useReducer, useState} from "react"; -import {CatList, getCatName} from "../../../utils/Tools.js"; +import {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"; function SelectReducer(state, action) { switch (action.type) { @@ -20,6 +21,11 @@ function SelectReducer(state, action) { return acc; }, {}) }; + case 'ADD_ID': + return { + ...state, + [action.payload]: false + }; case 'CLEAR_ACTIVE': const newState = {...state}; Object.keys(newState).forEach(id => { @@ -53,14 +59,15 @@ function SelectReducer(state, action) { } } -export function SelectCombModalContent({data, setGroups}) { +export function SelectCombModalContent({data, groups, setGroups, teamMode = false}) { const country = useCountries('fr') const {t} = useTranslation("cm"); - const {dispatch} = useWS() + const {sendRequest, dispatch} = useWS() const [dispo, dispoReducer] = useReducer(SelectReducer, {}) const [select, selectReducer] = useReducer(SelectReducer, {}) + const lastClick = useRef({time: 0, id: null}); - const [targetGroupe, setTargetGroupe] = useState("A") + const [targetGroupe, setTargetGroupe] = useState("1") const [search, setSearch] = useState("") const [country_, setCountry_] = useState("") const [club, setClub] = useState("") @@ -68,12 +75,27 @@ export function SelectCombModalContent({data, setGroups}) { const [cat, setCat] = useState(-1) const [weightMin, setWeightMin] = useState(0) const [weightMax, setWeightMax] = useState(0) + const [team, setTeam] = useState(false) + const [teamName, setTeamName] = useState(""); const handleSubmit = (e) => { e.preventDefault(); - setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => { - return {id: Number(id), poule: targetGroupe} - })]) + if (teamMode) { + toast.promise( + sendRequest('setTeam', { + name: teamName, + members: [...Object.keys(select).map(id => Number(id))] + }), getToastMessage("toast.team.update", "cm")) + .then(res => { + if (res && res.id) { + setGroups(prev => [...prev.filter(d => d.id !== Number(res.id)), {id: Number(res.id), poule: targetGroupe}]) + } + }) + } else { + setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => { + return {id: Number(id), poule: targetGroupe} + })]) + } dispoReducer({type: 'REMOVE_ALL'}) selectReducer({type: 'REMOVE_ALL'}) @@ -101,6 +123,18 @@ export function SelectCombModalContent({data, setGroups}) { dispoReducer({type: 'ADD_ALL', payload: data.map(d => d.id).filter(id => !selectedIds.includes(id))}) }, [data]) + useEffect(() => { + if (data == null) + return + const teamIds = data.filter(d => d.teamMembers != null && d.teamMembers.length > 0).map(t => String(t.id)); + if (teamMode) { + dispoReducer({type: 'REMOVE_IN', payload: teamIds}); + selectReducer({type: 'REMOVE_IN', payload: teamIds}); + } else { + dispoReducer({type: 'ADD_ALL', payload: teamIds}); + } + }, [teamMode]) + function applyFilter(dataIn, dataOut) { Object.keys(dataIn).forEach((id) => { const comb = data.find(d => d.id === Number(id)); @@ -113,7 +147,9 @@ export function SelectCombModalContent({data, setGroups}) { && (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA') && (cat === -1 || cat === 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)) { + && (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))) { dataOut[id] = dataIn[id]; } } @@ -155,7 +191,7 @@ export function SelectCombModalContent({data, setGroups}) { return <>
      -

      {t('select.sélectionnerDesCombatants')}

      +

      {t('select.sélectionnerDesCombatants')}{teamMode && (t('PourLéquipe'))}

      @@ -225,6 +261,16 @@ export function SelectCombModalContent({data, setGroups}) { })}
      + {!teamMode &&
      + +
      +
      + setTeam(e.target.checked)}/> + +
      +
      +
      }
      @@ -246,8 +292,19 @@ export function SelectCombModalContent({data, setGroups}) {
      {dispoFiltered && Object.keys(dispoFiltered).length === 0 &&
      {t('select.aucunCombattantDisponible')}
      } {Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => ( - ))}
      @@ -267,7 +324,16 @@ export function SelectCombModalContent({data, setGroups}) { {Object.keys(selectFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => ( ))}
      @@ -277,13 +343,19 @@ export function SelectCombModalContent({data, setGroups}) {
      - - + + setTeamName(e.target.value)}/> + } + + { - if (/^[a-zA-Z0-9]$/.test(e.target.value)) + if (/^[a-zA-Z0-9]?/.test(e.target.value)) setTargetGroupe(e.target.value) }}/> - +
      } diff --git a/src/main/webapp/src/pages/result/ResultView.jsx b/src/main/webapp/src/pages/result/ResultView.jsx index b5aed60..8beddfb 100644 --- a/src/main/webapp/src/pages/result/ResultView.jsx +++ b/src/main/webapp/src/pages/result/ResultView.jsx @@ -373,9 +373,9 @@ function CombResult({uuid, combId}) {

      {t('statistique')} :

      • {t('tauxDeVictoire2', { - nb: data.matchs.length === 0 ? "---" : (data.totalWin / data.matchs.length * 100).toFixed(0), + nb: data.matchs.length === 0 ? "---" : (data.totalWin / data.matchs.filter(m => m.end).length * 100).toFixed(0), victoires: data.totalWin, - matchs: data.matchs.length + matchs: data.matchs.filter(m => m.end).length })}
      • {t('pointsMarqués2', {nb: data.pointMake})}
      • @@ -401,7 +401,7 @@ function CombResult({uuid, combId}) { {match.poule} {match.adv} {scoreToString(match.score)} - {match.ratio.toFixed(3)} + {match.end && match.ratio.toFixed(3)} {match.win ? : (match.eq ? : "")} )} diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index a91cd3a..c9a6d56 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -19,6 +19,10 @@ export function isClubAdmin(userinfo) { export const errFormater = (data, msg) => { + if (!data) + return msg + if (!data.response) + return `${msg} (😕 ${data.message})` if (typeof data.response.data === 'string' || data.response.data instanceof String) return `${msg} (${data.response.statusText}: ${data.response.data}) 😕` return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕` @@ -107,13 +111,13 @@ export function getCatName(cat) { } } -export function getToastMessage(msgKey) { +export function getToastMessage(msgKey, ns = 'common') { return { - pending: i18n.t(msgKey + '.pending'), - success: i18n.t(msgKey + '.success'), + pending: i18n.t(msgKey + '.pending', {ns}), + success: i18n.t(msgKey + '.success', {ns}), error: { render({data}) { - return errFormater(data, i18n.t(msgKey + '.error')) + return errFormater(data, i18n.t(msgKey + '.error', {ns})) } } } @@ -148,7 +152,7 @@ export function timePrint(time, negSign = false) { if (time === null || time === undefined) return "" const neg = time < 0 - if (neg){ + if (neg) { if (!negSign) return "00:00" time = -time @@ -168,22 +172,22 @@ export function timePrint(time, negSign = false) { } //create full hex -function fullHex (hex) { - let r = hex.slice(1,2); - let g = hex.slice(2,3); - let b = hex.slice(3,4); +function fullHex(hex) { + let r = hex.slice(1, 2); + let g = hex.slice(2, 3); + let b = hex.slice(3, 4); - r = parseInt(r+r, 16); - g = parseInt(g+g, 16); - b = parseInt(b+b, 16); + r = parseInt(r + r, 16); + g = parseInt(g + g, 16); + b = parseInt(b + b, 16); // return {r, g, b} - return { r, g, b }; + return {r, g, b}; } //convert hex to rgb -export function hex2rgb (hex) { - if(hex.length === 4){ +export function hex2rgb(hex) { + if (hex.length === 4) { return fullHex(hex); } @@ -192,5 +196,5 @@ export function hex2rgb (hex) { const b = parseInt(hex.slice(5, 7), 16); // return {r, g, b} - return { r, g, b }; + return {r, g, b}; }