From 758e02dc5ba68cf1a265c45b5b2169fc1a6be715 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 6 Feb 2026 17:26:33 +0100 Subject: [PATCH 1/9] feat: remove ^/api$ path log --- .../java/fr/titionfire/ffsaf/FrontendForwardingFilter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/titionfire/ffsaf/FrontendForwardingFilter.java b/src/main/java/fr/titionfire/ffsaf/FrontendForwardingFilter.java index b8392bc..ec29c4a 100644 --- a/src/main/java/fr/titionfire/ffsaf/FrontendForwardingFilter.java +++ b/src/main/java/fr/titionfire/ffsaf/FrontendForwardingFilter.java @@ -44,7 +44,9 @@ public class FrontendForwardingFilter implements ContainerResponseFilter { final String path = info.getPath(); final String address = request.remoteAddress().toString(); - LOG.infof("Request %s %s from IP %s", method, path, address); + if (!path.equals("/api")) { + LOG.infof("Request %s %s from IP %s", method, path, address); + } int status = responseContext.getStatus(); if (status != 404 && !(status == 405 && "GET".equals(requestContext.getMethod()))) { From 8663aa61cfa48de8fefd079ae9f17bcd6c35f07c Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 6 Feb 2026 18:02:11 +0100 Subject: [PATCH 2/9] feat: add silver cup png to cm --- .../src/pages/competition/editor/CMTMatchPanel.jsx | 10 ++++++++-- .../pages/competition/editor/CategoryAdminContent.jsx | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 293ba7c..5fca965 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -31,6 +31,12 @@ function CupImg() { alt=""/> } +function CupImg2() { + return +} + export function CategorieSelect({catId, setCatId, menuActions}) { const setLoading = useLoadingSwitcher() const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); @@ -443,14 +449,14 @@ function MatchList({matches, cat, menuActions, classement = false, currentMatch {!classement && {m.poule}} {index >= firstIndex ? index + 1 - firstIndex : ""} - {m.end && m.win > 0 && } + {m.end && ((m.win > 0 && ) || (m.win === 0 && ))} - {m.end && m.win < 0 && } + {m.end && ((m.win < 0 && ) || (m.win === 0 && ))} ))} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 8fe0aff..ef17a2b 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -30,6 +30,11 @@ function CupImg() { style={{width: "16px"}} src="/img/171891.png" alt=""/> } +function CupImg2() { + return +} export function CategoryContent({cat, catId, setCat, menuActions}) { const setLoading = useLoadingSwitcher() @@ -712,7 +717,7 @@ function MatchList({matches, cat, groups, reducer, classement = false}) { {index + 1} {!classement && {m.poule}} {!classement && {liceName[index % liceName.length]}} - {m.end && m.win > 0 && } + {m.end && ((m.win > 0 && ) || (m.win === 0 && ))} handleCombClick(e, m.id, m.c1)}> @@ -721,7 +726,7 @@ function MatchList({matches, cat, groups, reducer, classement = false}) { onClick={e => handleCombClick(e, m.id, m.c2)}> - {m.end && m.win < 0 && } + {m.end && ((m.win < 0 && ) || (m.win === 0 && ))} {scoreToString2(m, cards_v)} handleEditMatch(m.id)}> From d43cdc1a4e904ed169981c05975dad5a25e4dccf Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 11 Feb 2026 14:08:13 +0100 Subject: [PATCH 3/9] feat: auto categories --- .../ffsaf/domain/entity/CombEntity.java | 8 +- .../domain/service/CompetitionService.java | 63 ++- .../titionfire/ffsaf/ws/recv/RCategorie.java | 6 + .../titionfire/ffsaf/ws/send/SRegister.java | 18 + src/main/webapp/public/locales/en/cm.json | 9 + src/main/webapp/public/locales/fr/cm.json | 11 +- .../src/components/cm/AutoCatModalContent.jsx | 477 +++++++++++++++--- .../src/components/cm/ListPresetSelect.jsx | 13 +- .../src/pages/competition/editor/CMAdmin.jsx | 59 ++- .../competition/editor/CMTMatchPanel.jsx | 4 +- .../editor/CategoryAdminContent.jsx | 6 +- 11 files changed, 571 insertions(+), 103 deletions(-) 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 b34bc13..e0da444 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java @@ -47,7 +47,8 @@ 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.getWeight2() != null ? model.getWeight2() : model.getWeight(), Stream.concat(model.getComb().stream().map(CombEntity::fromModel), model.getGuest().stream().map(CombEntity::fromModel)).toList(), new ArrayList<>()); @@ -68,7 +69,8 @@ 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<>(), - new ArrayList<>()); + model.getCountry(), registerModel.getOverCategory(), + registerModel.getWeight2() != null ? registerModel.getWeight2() : registerModel.getWeight(), + new ArrayList<>(), 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 110482e..1612bd5 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -368,6 +368,9 @@ public class CompetitionService { return Panache.withTransaction(() -> repository.persist(c)); }) .chain(combModel -> updateRegister(data, c, combModel, true, false))) + .call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? + sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom() + .voidItem()) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) .setCategorieInscrite(r.getCategoriesInscrites())); } else { @@ -406,8 +409,8 @@ public class CompetitionService { })) .chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model)) .call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? - sRegister.sendRegister(model.getCompetition().getUuid(), - r) : Uni.createFrom().voidItem())) + sRegister.sendRegisterNoFetch(model.getCompetition().getUuid(), r) + : Uni.createFrom().voidItem())) .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites())); } if ("club".equals(source)) @@ -428,6 +431,9 @@ public class CompetitionService { throw new DForbiddenException(trad.t("insc.err1")); })) .chain(combModel -> updateRegister(data, c, combModel, false, false))) + .call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? + sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom() + .voidItem()) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) .setCategorieInscrite(r.getCategoriesInscrites())); @@ -444,6 +450,8 @@ public class CompetitionService { throw new DForbiddenException(trad.t("insc.err2")); })) .chain(combModel -> updateRegister(data, c, combModel, false, false))) + .call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? + sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom().voidItem()) .map(r -> SimpleRegisterComb.fromModel(r, List.of()).setCategorieInscrite(r.getCategoriesInscrites())); } @@ -453,33 +461,33 @@ public class CompetitionService { if (!"admin".equals(source)) return Uni.createFrom().failure(new DForbiddenException()); - return Multi.createFrom().iterable(datas).onItem().transformToUni(data -> - makeImportUpdate(securityCtx, id, data).onFailure().recoverWithItem(t -> { - SimpleRegisterComb errorComb = new SimpleRegisterComb(); - errorComb.setLicence(-42); - errorComb.setFname("ERROR"); - errorComb.setLname(t.getMessage()); - return errorComb; - })).concatenate().collect().asList(); + return permService.hasEditPerm(securityCtx, id) + .chain(cm -> Multi.createFrom().iterable(datas).onItem().transformToUni(data -> + makeImportUpdate(cm, data).onFailure().recoverWithItem(t -> { + SimpleRegisterComb errorComb = new SimpleRegisterComb(); + errorComb.setLicence(-42); + errorComb.setFname("ERROR"); + errorComb.setLname(t.getMessage()); + return errorComb; + })).concatenate().collect().asList()); } - private Uni makeImportUpdate(SecurityCtx securityCtx, Long id, RegisterRequestData data) { + @WithSession + public Uni makeImportUpdate(CompetitionModel c, RegisterRequestData data) { if (data.getLicence() == null || data.getLicence() != -1) { // not a guest - return permService.hasEditPerm(securityCtx, id) - .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) - .call(combModel -> Mutiny.fetch(combModel.getLicences())) - .call(combModel -> { - if (c.getBanMembre() == null) - c.setBanMembre(new ArrayList<>()); - c.getBanMembre().remove(combModel.getId()); - return Panache.withTransaction(() -> repository.persist(c)); - }) - .chain(combModel -> updateRegister(data, c, combModel, true, true))) + return findComb(data.getLicence(), data.getFname(), data.getLname()) + .call(combModel -> Mutiny.fetch(combModel.getLicences())) + .call(combModel -> { + if (c.getBanMembre() == null) + c.setBanMembre(new ArrayList<>()); + c.getBanMembre().remove(combModel.getId()); + return Panache.withTransaction(() -> repository.persist(c)); + }) + .chain(combModel -> updateRegister(data, c, combModel, true, true)) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) .setCategorieInscrite(r.getCategoriesInscrites())); } else { - return permService.hasEditPerm(securityCtx, id) - .chain(c -> findGuestOrInit(data.getFname(), data.getLname(), c)) + return findGuestOrInit(data.getFname(), data.getLname(), c) .invoke(Unchecked.consumer(model -> { if (data.getCategorie() == null) throw new DBadRequestException(trad.t("categorie.requise")); @@ -503,13 +511,13 @@ public class CompetitionService { } else model.setCountry(data.getCountry()); - if (model.getCompetition().getRequiredWeight().contains(model.getCategorie())) { + if (c.getRequiredWeight().contains(model.getCategorie())) { if (data.getCountry() != null) model.setWeight(data.getWeight()); } })) .call(g -> Mutiny.fetch(g.getCategoriesInscrites())) - .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(), + .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", c, data.getCategoriesInscrites()) .invoke(cats -> { g.getCategoriesInscrites().clear(); @@ -518,9 +526,8 @@ public class CompetitionService { .noneMatch(e -> e.getCategorie().equals(g.getCategorie()))); })) .chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model)) - .call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? - sRegister.sendRegister(model.getCompetition().getUuid(), - r) : Uni.createFrom().voidItem())) + .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ? + sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem())) .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites())); } } 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 2f7dcf2..24ca75c 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -117,7 +117,13 @@ public class RCategorie { categoryModel.setTree(new ArrayList<>()); categoryModel.setType(categorie.type); categoryModel.setLiceName(categorie.liceName); + categoryModel.setTreeAreClassement(categorie.treeAreClassement); + categoryModel.setFullClassement(categorie.fullClassement); + if (categorie.preset() != null) + return catPresetRepository.findById(categorie.preset().getId()) + .invoke(categoryModel::setPreset) + .chain(__ -> categoryRepository.create(categoryModel)); return categoryRepository.create(categoryModel); }) .invoke(cat -> SSCategorie.sendAddCategory(connection, cat)) 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 9dd8384..853e3f2 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/send/SRegister.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SRegister.java @@ -40,6 +40,16 @@ public class SRegister { .chain(cardModels -> send(uuid, "sendCards", cardModels)))); } + public Uni sendRegisterNoFetch(String uuid, RegisterModel registerModel) { + return send(uuid, "sendRegister", + CombEntity.fromModel(registerModel).addCategoriesInscrites(registerModel.getCategoriesInscrites())) + .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 Mutiny.fetch(model.getCategoriesInscrites()).chain(o -> send(uuid, "sendRegister", CombEntity.fromModel(model).addCategoriesInscrites(o)) @@ -48,6 +58,14 @@ public class SRegister { .chain(cardModels -> send(uuid, "sendCards", cardModels)))); } + public Uni sendRegisterNoFetch(String uuid, CompetitionGuestModel model) { + return send(uuid, "sendRegister", + CombEntity.fromModel(model).addCategoriesInscrites(model.getCategoriesInscrites())) + .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) .call(__ -> cardService.rmTeamCardFromComb(combId, uuid)); diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index 5ccf2bc..da85ab4 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -23,7 +23,9 @@ "cartonNoir": "Black card", "cartonRouge": "Red card", "catégorie": "Category", + "catégoriesVontêtreCréées": "weight categories will be created", "ceCartonEstIssuDunCartonDéquipe": "This card comes from a team card, do you really want to delete it?", + "certainsCombattantsNontPasDePoidsRenseigné": "Some fighters do not have a weight listed; they will NOT be included in the categories.", "chrono.+/-...S": "+/- ... s", "chrono.+10S": "+10 s", "chrono.+1S": "+1 s", @@ -58,11 +60,15 @@ "conserverUniquementLesMatchsTerminés": "Keep only finished matches", "contre": "vs", "couleur": "Color", + "créationDeLaLesCatégories": "Creating the category(ies)", + "créerLaPhaseFinaleSilYADesPoules": "Create the final phase if there are groups.", "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", + "créerToutesLesCatégories": "Create all categories", "date": "Date", "demi-finalesEtFinales": "Semi-finals and finals", + "depuisUneCatégoriePrédéfinie": "From a predefined category", "duréePause": "Pause duration", "duréeRound": "Round duration", "editionDeLaCatégorie": "Edit category", @@ -88,11 +94,13 @@ "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.", + "lesCombattantsEnDehors2": "Fighters outside the ranking tournament will have a ranking match", "listeDesCartons": "List of cards", "manche": "Round", "matchPourLesPerdantsDuTournoi": "Match for tournament losers:", "matchTerminé": "Match over", "matches": "Matches", + "modeDeCréation": "Creation method", "modifier": "Edit", "msg1": "There are already matches in this pool; what do you want to do with them?", "neRienConserver": "Keep nothing", @@ -105,6 +113,7 @@ "nouvelle...": "New...", "obs.préfixDesSources": "Source prefix", "pays": "Country", + "personnaliser": "Personalize", "poids": "Weight", "poule": "Pool", "poulePour": "Pool for: ", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 384acfa..4f8f446 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -23,7 +23,9 @@ "cartonNoir": "Carton noir", "cartonRouge": "Carton rouge", "catégorie": "Catégorie", + "catégoriesVontêtreCréées": "catégories de poids vont être créées", "ceCartonEstIssuDunCartonDéquipe": "Ce carton est issu d'un carton d'équipe, voulez-vous vraiment le supprimer ?", + "certainsCombattantsNontPasDePoidsRenseigné": "Certains combattants n'ont pas de poids renseigné, ils ne seront PAS insert dans les catégories", "chrono.+/-...S": "+/- ... s", "chrono.+10S": "+10 s", "chrono.+1S": "+1 s", @@ -38,7 +40,7 @@ "chronomètre": "Chronomètre", "classement": "Classement", "club": "Club", - "combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélectionnés ci-dessus.", + "combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélections ci-dessus.", "compétition": "Compétition", "compétitionManager": "Compétition manager", "config.obs.dossierDesResources": "Dossier des resources", @@ -58,11 +60,15 @@ "conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés", "contre": "contre", "couleur": "Couleur", + "créationDeLaLesCatégories": "Création de la/les catégories", + "créerLaPhaseFinaleSilYADesPoules": "Créer la phase finale s'il y a des poules", "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", + "créerToutesLesCatégories": "Créer toutes les catégories", "date": "Date", "demi-finalesEtFinales": "Demi-finales et finales", + "depuisUneCatégoriePrédéfinie": "Depuis une catégorie prédéfinie", "duréePause": "Durée pause", "duréeRound": "Durée round", "editionDeLaCatégorie": "Edition de la catégorie", @@ -88,11 +94,13 @@ "inscrit": "Inscrit", "leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules", "lesCombattantsEnDehors": "Les combattants en dehors du tournoi auront un match de classement", + "lesCombattantsEnDehors2": "Les combattants en dehors du tournoi de classement auront un match de classement", "listeDesCartons": "Liste des cartons", "manche": "Manche", "matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:", "matchTerminé": "Match terminé", "matches": "Matches", + "modeDeCréation": "Mode de création", "modifier": "Modifier", "msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?", "neRienConserver": "Ne rien conserver", @@ -105,6 +113,7 @@ "nouvelle...": "Nouvelle...", "obs.préfixDesSources": "Préfix des sources", "pays": "Pays", + "personnaliser": "Personnaliser", "poids": "Poids", "poule": "Poule", "poulePour": "Poule pour: ", diff --git a/src/main/webapp/src/components/cm/AutoCatModalContent.jsx b/src/main/webapp/src/components/cm/AutoCatModalContent.jsx index bd9e1ee..8584740 100644 --- a/src/main/webapp/src/components/cm/AutoCatModalContent.jsx +++ b/src/main/webapp/src/components/cm/AutoCatModalContent.jsx @@ -1,8 +1,13 @@ import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; +import {Trans, useTranslation} from "react-i18next"; import {useCountries} from "../../hooks/useCountries.jsx"; import {ListPresetSelect} from "./ListPresetSelect.jsx"; import {CatList, getCatName} from "../../utils/Tools.js"; +import {useCombs} from "../../hooks/useComb.jsx"; +import {toast} from "react-toastify"; +import {build_tree} from "../../utils/TreeUtils.js"; +import {createMatch} from "../../utils/CompetitionTools.js"; +import {useWS} from "../../hooks/useWS.jsx"; export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) { const country = useCountries('fr') @@ -52,64 +57,6 @@ export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1 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(); @@ -230,3 +177,415 @@ export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1 } + + +function 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; +} + +function makeWeightCategories(combs) { + combs = combs.filter(c => c.weight != null).sort((a, b) => a.weight - b.weight); // Add random for same weight ? + const catCount = Math.ceil(combs.length / 10); + const catSize = combs.length / catCount; + const catMaxSize = Math.min(Math.ceil(catSize), 10); + const catMinSize = Math.max(Math.floor(catSize), 3); // Add marge ? + + const categories = Array.from({length: catCount}, () => []); + for (let i = 0; i < combs.length; i++) { + categories[Math.floor(i / catSize)].push(combs[i]); + } + + let change = false; + let maxIterations = 500; + do { + change = false; + + // ------ move in upper direction if better and possible ------ + + let needFree = -1; + let dIfFree = 0; + for (let i = 0; i < catCount - 1; i++) { + const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight; + const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight; + + if (weightDiff > nextWeightDiff && categories.at(i).length > catMinSize) { + if (categories.at(i + 1).length < catMaxSize) { + const movedComb = categories.at(i).pop(); + categories.at(i + 1).unshift(movedComb); + change = true; + } else if (weightDiff - nextWeightDiff > dIfFree) { + needFree = i; + dIfFree = weightDiff - nextWeightDiff; + } + } + } + if (needFree !== -1) { + let haveSpace = -1; + let maxDiff = 0; + for (let i = needFree + 1; i < catCount; i++) { + if (categories.at(i).length < catMaxSize) { + haveSpace = i; + break; + } + } + if (haveSpace !== -1) { + for (let i = needFree + 1; i < haveSpace; i++) { + const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight; + const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight; + const diffIfFree = weightDiff - nextWeightDiff; + if (diffIfFree > maxDiff) { + maxDiff = diffIfFree; + } + } + + if (maxDiff < dIfFree) { + for (let i = needFree; i < haveSpace; i++) { + const movedComb = categories.at(i).pop(); + categories.at(i + 1).unshift(movedComb); + change = true; + } + } + } + } + + // ------ move in lower direction if better and possible ------ + + needFree = -1; + dIfFree = 0; + for (let i = 1; i < catCount; i++) { + const currentFirst = categories[i][0]; + const currentSecondFirst = categories[i][1]; + const prevLast = categories[i - 1][categories[i - 1].length - 1]; + + const weightDiff = currentSecondFirst.weight - currentFirst.weight; + const prevWeightDiff = currentFirst.weight - prevLast.weight; + + if (weightDiff > prevWeightDiff && categories.at(i).length > catMinSize) { + if (categories.at(i - 1).length < catMaxSize) { + const movedComb = categories.at(i).shift(); + categories.at(i - 1).push(movedComb); + change = true; + } else if (weightDiff - prevWeightDiff > dIfFree) { + needFree = i; + dIfFree = weightDiff - prevWeightDiff; + } + } + } + if (needFree !== -1) { + let haveSpace = -1; + let maxDiff = 0; + for (let i = needFree - 1; i >= 0; i--) { + if (categories.at(i).length < catMaxSize) { + haveSpace = i; + break; + } + } + if (haveSpace !== -1) { + for (let i = needFree - 1; i > haveSpace; i--) { + const currentFirst = categories[i][0]; + const currentSecondFirst = categories[i][1]; + const prevLast = categories[i - 1][categories[i - 1].length - 1]; + + const weightDiff = currentSecondFirst.weight - currentFirst.weight; + const prevWeightDiff = currentFirst.weight - prevLast.weight; + + const diffIfFree = weightDiff - prevWeightDiff; + if (diffIfFree > maxDiff) { + maxDiff = diffIfFree; + } + } + + if (maxDiff < dIfFree) { + for (let i = needFree; i > haveSpace; i--) { + const movedComb = categories.at(i).shift(); + categories.at(i - 1).push(movedComb); + change = true; + } + } + } + } + } while (change && maxIterations-- > 0); + + return categories; +} + +const getCatNameList = (count) => { + const catNameList = []; + if (count >= 10) catNameList.push("Paille"); + if (count >= 9) catNameList.push("Mouche"); + if (count >= 8) catNameList.push("Coq"); + if (count >= 7) catNameList.push("Plume"); + if (count >= 3) catNameList.push("Léger"); + if (count >= 5) catNameList.push("Mi-moyen"); + if (count >= 1) catNameList.push("Moyen"); + if (count >= 6) catNameList.push("Mi-lourd"); + if (count >= 2) catNameList.push("Lourd"); + if (count >= 4) catNameList.push("Super-lourd"); + + return catNameList; +} + +export function AutoNewCatModalContent() { + const {t} = useTranslation("cm"); + const {combs} = useCombs(); + const {sendRequest} = useWS(); + const toastId = React.useRef(null); + + const [gender, setGender] = useState({H: false, F: false, NA: false}) + const [cat, setCat] = useState([]) + const [preset, setPreset] = useState(undefined) + const [lice, setLice] = useState("1") + const [classement, setClassement] = useState(true) + const [fullClassement, setFullClassement] = useState(false) + + 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 => { + if (comb == null) + return; + if ((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))) + && (preset === undefined || comb.categoriesInscrites.includes(preset.id))) { + dataOut.push(comb) + } + } + ) + } + + const dispoFiltered = []; + if (combs != null) + applyFilter(Object.values(combs), dispoFiltered); + + const handleSubmit = (e) => { + e.preventDefault(); + + let catList + if (dispoFiltered.length > 10) { + catList = makeWeightCategories(dispoFiltered); + } else { + catList = [[...dispoFiltered]]; + } + console.log(catList.map(c => c.map(c => ({id: c.id, weight: c.weight, fname: c.fname, lname: c.lname})))) + + toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0}); + + new Promise(async (resolve) => { + for (let i = 0; i < catList.length; i++) { + const progress = (i + 1) / catList.length; + toast.update(toastId.current, {progress}); + + const g = [] + if (gender.H) g.push('H'); + if (gender.F) g.push('F'); + + const type = catList[i].length > 5 && classement ? 3 : 1; + const newCat = { + name: preset.name + " - " + cat.map(pos => getCatName(CatList[pos])).join(", ") + + (gender.H && gender.F ? "" : " - " + g.join("/")) + (catList.length === 1 ? "" : " - " + getCatNameList(catList.length)[i]), + liceName: lice, + type: type, + treeAreClassement: classement, + fullClassement: fullClassement, + preset: {id: preset.id} + } + console.log(newCat) + + await sendRequest('createCategory', newCat).then(id => { + newCat["id"] = id; + const groups = makePoule(catList[i], []); + const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups); + + const p = []; + p.push(sendRequest("recalculateMatch", { + categorie: newCat.id, + newMatch, + matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), + matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), + matchesToRemove: [] + }).then(() => { + console.log("Finished creating matches for category", newCat.name); + }).catch(err => { + console.error("Error creating matches for category", newCat.name, err); + })) + + if (type === 3) { + const trees = build_tree(4, 1) + console.log("Creating trees for new category:", trees); + + p.push(sendRequest('updateTrees', { + categoryId: id, + trees: trees + }).then(() => { + console.log("Finished creating trees for category", newCat.name); + }).catch(err => { + console.error("Error creating trees for category", newCat.name, err); + })) + } + + return Promise.allSettled(p) + }).catch(err => { + console.error("Error creating category", newCat.name, err); + }) + console.log("Finished category", i + 1, "/", catList.length); + } + resolve(); + }).finally(() => { + toast.done(toastId.current); + }) + } + + return <> +
+

{t('depuisUneCatégoriePrédéfinie')}

+ +
+
+
+ + +
+ +
+
+ setGender((prev) => { + return {...prev, H: e.target.checked} + })}/> + +
+
+ setGender((prev) => { + return {...prev, F: e.target.checked} + })}/> + +
+
+ setGender((prev) => { + return {...prev, NA: e.target.checked} + })}/> + +
+
+
+
+ {preset !== undefined && <> +
+
+ + {preset.categories.map(c => [c.categorie, CatList.indexOf(c.categorie)]).sort((a, b) => a[1] - b[1]) + .map(([cat_, index]) => { + return
+
+ setCat_(e, index)}/> + +
+
+ })} +
+
+ } + +
+ + setLice(e.target.value)}/> +
+ +
+ setClassement(e.target.checked)}/> + +
+
+ setFullClassement(e.target.checked)}/> + +
+ + {dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')}
+ {Math.ceil(dispoFiltered.length / 10)} {t('catégoriesVontêtreCréées')}
+ {dispoFiltered.length > 10 && dispoFiltered.some(c => !c.weight) && + {t('certainsCombattantsNontPasDePoidsRenseigné')}} + +
+
+ + +
+ +} diff --git a/src/main/webapp/src/components/cm/ListPresetSelect.jsx b/src/main/webapp/src/components/cm/ListPresetSelect.jsx index eb7b371..6212e53 100644 --- a/src/main/webapp/src/components/cm/ListPresetSelect.jsx +++ b/src/main/webapp/src/components/cm/ListPresetSelect.jsx @@ -3,7 +3,7 @@ import {AxiosError} from "../AxiosError.jsx"; import {useTranslation} from "react-i18next"; import React, {useId} from "react"; -export function ListPresetSelect({disabled, value, onChange}) { +export function ListPresetSelect({disabled, value, onChange, returnId = true}) { const id = useId() const {data, error} = useRequestWS("listPreset", {}, null); const {t} = useTranslation(); @@ -12,9 +12,16 @@ export function ListPresetSelect({disabled, value, onChange}) { ?
: error diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 1cdcf1e..cfd9c5d 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -20,6 +20,7 @@ import {StateWindow} from "./StateWindow.jsx"; import {CombName, useCombs} from "../../../hooks/useComb.jsx"; import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; +import {AutoNewCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -504,8 +505,9 @@ function CategoryHeader({ cat, setCatId }) { const setLoading = useLoadingSwitcher() - const bthRef = useRef(); - const confirmRef = useRef(); + const bthRef = useRef(null); + const newBthRef = useRef(null); + const confirmRef = useRef(null); const [modal, setModal] = useState({}) const [confirm, setConfirm] = useState({}) const {t} = useTranslation("cm"); @@ -521,6 +523,7 @@ function CategoryHeader({ ]) } const sendAddCategory = ({data}) => { + console.log("add cat", data); setCats([...cats, data]) } const sendDelCategory = ({data}) => { @@ -554,8 +557,7 @@ function CategoryHeader({ if (selectedCatId !== "-1") { setCatId(selectedCatId); } else { // New category - setModal({}); - bthRef.current.click(); + newBthRef.current.click(); e.target.value = cat?.id; } } @@ -595,10 +597,58 @@ function CategoryHeader({ + + + + + { }} onCancel={confirm.cancel ? confirm.cancel : () => { }} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/> + + + + + } @@ -617,6 +667,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) { const {sendRequest} = useWS(); useEffect(() => { + console.log(state); setName(state.name || ""); setLice(state.liceName || "1"); setPoule(((state.type || 1) & 1) !== 0); diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 5fca965..feef8ab 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -317,9 +317,9 @@ function MatchList({matches, cat, menuActions, classement = false, currentMatch const liceName = (cat.liceName || "N/A").split(";"); const marches2 = classement - ? matches.filter(m => m.categorie_ord === -42) + ? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id) .map(m => ({...m, ...win_end(m, cards_v)})) - : matches.filter(m => m.categorie_ord !== -42) + : matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id) .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; diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index ef17a2b..96ca086 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -433,7 +433,7 @@ function ListMatch({cat, matches, groups, reducer}) { matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), matchesToRemove: matchesToRemove.map(m => m.id) - }), getToastMessage("toast.matchs.create", "ns")) + }), getToastMessage("toast.matchs.create", "cm")) .finally(() => { console.log("Finished creating matches"); }) @@ -521,9 +521,9 @@ function MatchList({matches, cat, groups, reducer, classement = false}) { const liceName = (cat.liceName || "N/A").split(";"); const marches2 = classement - ? matches.filter(m => m.categorie_ord === -42) + ? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id) .map(m => ({...m, ...win_end(m, cards_v)})) - : matches.filter(m => m.categorie_ord !== -42) + : matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id) .sort((a, b) => a.categorie_ord - b.categorie_ord) .map(m => ({...m, ...win_end(m, cards_v)})) From 2fd09af0ea28d66b2d65410778d0bc0032d2b3a4 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 13 Feb 2026 21:33:19 +0100 Subject: [PATCH 4/9] feat: auto categories full competition --- .../titionfire/ffsaf/ws/recv/RCategorie.java | 33 +- .../src/components/cm/AutoCatModalContent.jsx | 353 ++++++++++++++---- .../src/pages/competition/editor/CMAdmin.jsx | 12 +- .../editor/CategoryAdminContent.jsx | 10 +- .../editor/SelectCombModalContent.jsx | 2 +- 5 files changed, 329 insertions(+), 81 deletions(-) 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 24ca75c..c118ece 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -29,6 +29,7 @@ import lombok.Data; import org.hibernate.reactive.mutiny.Mutiny; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; @WithSession @@ -130,9 +131,39 @@ public class RCategorie { .map(CategoryModel::getId); } + @WSReceiver(code = "createOrReplaceCategory", permission = PermLevel.ADMIN) + public Uni createOrReplaceCategory(WebSocketConnection connection, JustCategorie categorie) { + return matchRepository.list("category.compet.uuid = ?1 AND category.name = ?2", connection.pathParam("uuid"), + categorie.name) + .chain(existing -> { + if (existing.isEmpty()) + return createCategory(connection, categorie); + + Map> matchesByCategory = existing.stream() + .filter(m -> m.getCategory() != null) + .collect(Collectors.groupingBy(m -> m.getCategory().getId())); + + for (Map.Entry> entry : matchesByCategory.entrySet()) { + Long categoryId = entry.getKey(); + List matches = entry.getValue(); + + if (matches.stream().noneMatch(m -> !m.getScores().isEmpty() || m.isEnd())) + return Panache.withTransaction(() -> updateCategory(connection, categorie, categoryId) + .call(__ -> treeRepository.delete("category = ?1", categoryId)) + .call(__ -> matchRepository.delete("category.id = ?1", categoryId))) + .replaceWith(categoryId); + } + return createCategory(connection, categorie); + }); + } + @WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN) public Uni updateCategory(WebSocketConnection connection, JustCategorie categorie) { - return getById(categorie.id, connection) + return updateCategory(connection, categorie, categorie.id); + } + + private Uni updateCategory(WebSocketConnection connection, JustCategorie categorie, Long id) { + return getById(id, connection) .call(cat -> { if (categorie.preset() == null) { cat.setPreset(null); diff --git a/src/main/webapp/src/components/cm/AutoCatModalContent.jsx b/src/main/webapp/src/components/cm/AutoCatModalContent.jsx index 8584740..0bd0303 100644 --- a/src/main/webapp/src/components/cm/AutoCatModalContent.jsx +++ b/src/main/webapp/src/components/cm/AutoCatModalContent.jsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from "react"; +import React, {useEffect, useId, useState} from "react"; import {Trans, useTranslation} from "react-i18next"; import {useCountries} from "../../hooks/useCountries.jsx"; import {ListPresetSelect} from "./ListPresetSelect.jsx"; @@ -7,7 +7,8 @@ import {useCombs} from "../../hooks/useComb.jsx"; import {toast} from "react-toastify"; import {build_tree} from "../../utils/TreeUtils.js"; import {createMatch} from "../../utils/CompetitionTools.js"; -import {useWS} from "../../hooks/useWS.jsx"; +import {useRequestWS, useWS} from "../../hooks/useWS.jsx"; +import {AxiosError} from "../AxiosError.jsx"; export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) { const country = useCountries('fr') @@ -369,16 +370,185 @@ const getCatNameList = (count) => { if (count >= 9) catNameList.push("Mouche"); if (count >= 8) catNameList.push("Coq"); if (count >= 7) catNameList.push("Plume"); - if (count >= 3) catNameList.push("Léger"); + if (count >= 2) catNameList.push("Léger"); if (count >= 5) catNameList.push("Mi-moyen"); - if (count >= 1) catNameList.push("Moyen"); + if (count >= 3) catNameList.push("Moyen"); if (count >= 6) catNameList.push("Mi-lourd"); - if (count >= 2) catNameList.push("Lourd"); + if (count >= 1) catNameList.push("Lourd"); if (count >= 4) catNameList.push("Super-lourd"); return catNameList; } +function makeCategory(combs) { + const out = Array.from(CatList, (v, i) => ({ + h: combs.filter(c => c.categorie === v && c.genre !== "F"), + f: combs.filter(c => c.categorie === v && c.genre === "F"), + m: [], + canMakeGenreFusion: i <= CatList.indexOf("BENJAMIN"), + c: v, + c_index: i, + min_c_index: i, + done: false + })) + + for (let i = 0; i < out.length; i++) + out[i].done = out[i].h.length === 0 && out[i].f.length === 0; + + for (let i = 0; i < out.length - 1; i++) { + const p = i === 0 ? undefined : out[i - 1]; + const c = out[i]; + const n = out[i + 1]; + + if (c.done) + continue; + if (c.canMakeGenreFusion) { + if (c.f.length < 6 || c.h.length < 5) { + if (c.f.length + c.h.length >= 3) { + c.m = c.h.concat(c.f); + c.h = []; + c.f = []; + c.done = true; + } else { + n.h = n.h.concat(c.h); + n.f = n.f.concat(c.f); + n.min_c_index = c.min_c_index + c.h = []; + c.f = []; + c.done = true; + } + } else { + c.done = true; + } + } else { + if (c.h.length < 3 && c.h.length > 0) { + if (p) { + if (p.h.length > 0 && p.h.length + c.h.length <= c.h.length + n.h.length && p.min_c_index - p.c_index < 1) { + p.h = p.h.concat(c.h); + c.h = []; + } else { + n.h = n.h.concat(c.h); + c.h = []; + } + } else { + n.h = n.h.concat(c.h); + c.h = []; + } + } + if (c.f.length < 3 && c.f.length > 0) { + if (p) { + if (p.f.length > 0 && p.f.length + c.f.length <= c.f.length + n.f.length && p.min_c_index - p.c_index < 1) { + p.f = p.f.concat(c.f); + c.f = []; + } else { + n.f = n.f.concat(c.f); + c.f = []; + } + } else { + n.f = n.f.concat(c.f); + c.f = []; + } + } + c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0); + } + } + + // Down fusion if not done + for (let i = out.length - 1; i > 0; i--) { + const p = out[i - 1]; + const c = out[i]; + + if (c.done) + continue; + if (c.h.length > 0 && c.h.length < 3) { + p.h = p.h.concat(c.h); + c.h = []; + } + if (c.f.length > 0 && c.f.length < 3) { + p.f = p.f.concat(c.f); + c.f = []; + } + c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0); + p.done = (p.h.length >= 3 || p.h.length === 0) && (p.f.length >= 3 || p.f.length === 0); + } + + return out.map(c => [c.h, c.f, c.m]).flat().filter(l => l.length > 0); +} + +function sendCatList(toastId, t, catList, sendRequest) { + toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0}); + + new Promise(async (resolve) => { + for (let i = 0; i < catList.length; i++) { + const progress = (i + 1) / catList.length; + toast.update(toastId.current, {progress}); + + const g = [] + if (catList[i].combs.some(c => c.genre === "H")) g.push('H'); + if (catList[i].combs.some(c => c.genre === "F")) g.push('F'); + + const cat = [] + catList[i].combs.forEach(c => { + if (!cat.includes(c.categorie)) + cat.push(c.categorie); + }) + + const type = catList[i].combs.length > 5 && catList[i].classement ? 3 : 1; + const newCat = { + name: catList[i].preset.name + " - " + cat.map(c => getCatName(c)).join(", ") + + (g.length === 2 ? "" : " - " + g.join("/")) + (catList[i].size === 1 ? "" : " - " + getCatNameList(catList[i].size)[catList[i].index]), + liceName: catList[i].lice, + type: type, + treeAreClassement: catList[i].classement, + fullClassement: catList[i].fullClassement, + preset: {id: catList[i].preset.id} + } + console.log(newCat) + + await sendRequest('createOrReplaceCategory', newCat).then(id => { + newCat["id"] = id; + const groups = makePoule(catList[i].combs, []); + const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups); + + const p = []; + p.push(sendRequest("recalculateMatch", { + categorie: newCat.id, + newMatch, + matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), + matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), + matchesToRemove: [] + }).then(() => { + console.log("Finished creating matches for category", newCat.name); + }).catch(err => { + console.error("Error creating matches for category", newCat.name, err); + })) + + if (type === 3) { + const trees = build_tree(4, 1) + console.log("Creating trees for new category:", trees); + + p.push(sendRequest('updateTrees', { + categoryId: id, + trees: trees + }).then(() => { + console.log("Finished creating trees for category", newCat.name); + }).catch(err => { + console.error("Error creating trees for category", newCat.name, err); + })) + } + + return Promise.allSettled(p) + }).catch(err => { + console.error("Error creating category", newCat.name, err); + }) + console.log("Finished category", i + 1, "/", catList.length); + } + resolve(); + }).finally(() => { + toast.done(toastId.current); + }) +} + export function AutoNewCatModalContent() { const {t} = useTranslation("cm"); const {combs} = useCombs(); @@ -408,7 +578,7 @@ export function AutoNewCatModalContent() { return; if ((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))) - && (preset === undefined || comb.categoriesInscrites.includes(preset.id))) { + && (preset === undefined || comb.categoriesInscrites?.includes(preset.id))) { dataOut.push(comb) } } @@ -430,71 +600,8 @@ export function AutoNewCatModalContent() { } console.log(catList.map(c => c.map(c => ({id: c.id, weight: c.weight, fname: c.fname, lname: c.lname})))) - toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0}); - - new Promise(async (resolve) => { - for (let i = 0; i < catList.length; i++) { - const progress = (i + 1) / catList.length; - toast.update(toastId.current, {progress}); - - const g = [] - if (gender.H) g.push('H'); - if (gender.F) g.push('F'); - - const type = catList[i].length > 5 && classement ? 3 : 1; - const newCat = { - name: preset.name + " - " + cat.map(pos => getCatName(CatList[pos])).join(", ") + - (gender.H && gender.F ? "" : " - " + g.join("/")) + (catList.length === 1 ? "" : " - " + getCatNameList(catList.length)[i]), - liceName: lice, - type: type, - treeAreClassement: classement, - fullClassement: fullClassement, - preset: {id: preset.id} - } - console.log(newCat) - - await sendRequest('createCategory', newCat).then(id => { - newCat["id"] = id; - const groups = makePoule(catList[i], []); - const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups); - - const p = []; - p.push(sendRequest("recalculateMatch", { - categorie: newCat.id, - newMatch, - matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), - matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), - matchesToRemove: [] - }).then(() => { - console.log("Finished creating matches for category", newCat.name); - }).catch(err => { - console.error("Error creating matches for category", newCat.name, err); - })) - - if (type === 3) { - const trees = build_tree(4, 1) - console.log("Creating trees for new category:", trees); - - p.push(sendRequest('updateTrees', { - categoryId: id, - trees: trees - }).then(() => { - console.log("Finished creating trees for category", newCat.name); - }).catch(err => { - console.error("Error creating trees for category", newCat.name, err); - })) - } - - return Promise.allSettled(p) - }).catch(err => { - console.error("Error creating category", newCat.name, err); - }) - console.log("Finished category", i + 1, "/", catList.length); - } - resolve(); - }).finally(() => { - toast.done(toastId.current); - }) + sendCatList(toastId, t, catList + .map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})), sendRequest); } return <> @@ -589,3 +696,107 @@ export function AutoNewCatModalContent() { } + + +export function AutoNewCatSModalContent() { + const {t} = useTranslation("cm"); + const {combs} = useCombs(); + const {sendRequest} = useWS(); + const toastId = React.useRef(null); + const {data, error} = useRequestWS("listPreset", {}, null); + + const id = useId() + const [categories, setCategories] = useState([]) + const [lice, setLice] = useState("1") + const [classement, setClassement] = useState(true) + const [fullClassement, setFullClassement] = useState(false) + + const setCategories_ = (e, catId) => { + if (e.target.checked) { + if (!categories.includes(catId)) { + setCategories([...categories, catId]) + } + } else { + setCategories(categories.filter(c => c !== catId)) + } + } + + const handleSubmit = (e) => { + e.preventDefault(); + + let catList2 = [] + for (const catId of categories) { + const preset = data.find(p => p.id === catId); + const dispoFiltered = Object.values(combs).filter(comb => comb.categoriesInscrites?.includes(catId)).sort(() => Math.random() - 0.5) + .map(comb => ({...comb, categorie: CatList[Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)]})); + console.log("Creating category for preset", preset.name, "and", dispoFiltered.length, "combattants"); + + const catList = makeCategory(dispoFiltered); + console.log(catList) + + for (const list of catList) { + if (list.length > 10) { + catList2.push(...makeWeightCategories(list) + .map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length}))); + } else { + catList2.push(({combs: [...list], classement, preset, lice, fullClassement, index: 1, size: 1})); + } + } + } + sendCatList(toastId, t, catList2, sendRequest); + } + + return <> +
+

{t('créerToutesLesCatégories')}

+ +
+
+
+
+ + {error ? : <> + {data && data.length === 0 &&
{t('aucuneCatégorieDisponible')}
} + {data && data.map((cat, index) => +
+
+ setCategories_(e, cat.id)}/> + +
+
)} + } +
+
+ +
+ + setLice(e.target.value)}/> +
+ +
+ setClassement(e.target.checked)}/> + +
+
+ setFullClassement(e.target.checked)}/> + +
+
+
+ + +
+ +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index cfd9c5d..876ebda 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -20,7 +20,7 @@ import {StateWindow} from "./StateWindow.jsx"; import {CombName, useCombs} from "../../../hooks/useComb.jsx"; import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; -import {AutoNewCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; +import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -597,8 +597,7 @@ function CategoryHeader({ - - diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 96ca086..fa0feaa 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -30,6 +30,7 @@ function CupImg() { style={{width: "16px"}} src="/img/171891.png" alt=""/> } + function CupImg2() { return d != null)}}); + if (data.categorie !== cat.id) + continue; + setGroups(prev => { if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id)) return [...prev, {id: data.c1?.id, poule: data.poule}]; @@ -717,7 +721,8 @@ function MatchList({matches, cat, groups, reducer, classement = false}) { {index + 1} {!classement && {m.poule}} {!classement && {liceName[index % liceName.length]}} - {m.end && ((m.win > 0 && ) || (m.win === 0 && ))} + {m.end && ((m.win > 0 && + ) || (m.win === 0 && ))} handleCombClick(e, m.id, m.c1)}> @@ -726,7 +731,8 @@ function MatchList({matches, cat, groups, reducer, classement = false}) { onClick={e => handleCombClick(e, m.id, m.c2)}> - {m.end && ((m.win < 0 && ) || (m.win === 0 && ))} + {m.end && ((m.win < 0 && + ) || (m.win === 0 && ))} {scoreToString2(m, cards_v)} handleEditMatch(m.id)}> diff --git a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx index 49cc6ef..a3fe039 100644 --- a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx @@ -156,7 +156,7 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals && (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)) - && (preset === -1 || comb.categoriesInscrites.includes(preset))) { + && (preset === -1 || comb.categoriesInscrites?.includes(preset))) { dataOut[id] = dataIn[id]; } } From d857fce71fe9f4c0b0432f03806a7a75cf1ce645 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 13 Feb 2026 21:42:48 +0100 Subject: [PATCH 5/9] feat: support decimal on weight --- .../ffsaf/data/model/CompetitionGuestModel.java | 6 +++--- .../fr/titionfire/ffsaf/data/model/RegisterModel.java | 8 ++++---- .../fr/titionfire/ffsaf/domain/entity/CombEntity.java | 2 +- .../fr/titionfire/ffsaf/rest/data/CompetitionData.java | 2 +- .../titionfire/ffsaf/rest/data/RegisterRequestData.java | 4 ++-- .../fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java | 4 ++-- src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java | 4 ++-- .../src/pages/competition/CompetitionRegisterAdmin.jsx | 4 ++-- src/main/webapp/src/pages/competition/CompetitionView.jsx | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) 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 fae3a91..2228f83 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java @@ -43,8 +43,8 @@ public class CompetitionGuestModel implements CombModel { String country = "fr"; - Integer weight = null; - Integer weightReal = null; + Float weight = null; + Float weightReal = null; @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinTable( @@ -109,7 +109,7 @@ public class CompetitionGuestModel implements CombModel { return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_)); } - public Integer getWeight2() { + public Float getWeight2() { return (this.weightReal != null) ? this.weightReal : this.weight; } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 75c03bd..8a3680c 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -37,8 +37,8 @@ public class RegisterModel { @JoinColumn(name = "id_membre") MembreModel membre; - Integer weight; - Integer weightReal; + Float weight; + Float weightReal; int overCategory = 0; Categorie categorie; @@ -61,7 +61,7 @@ public class RegisterModel { ) List categoriesInscrites = new ArrayList<>(); - public RegisterModel(CompetitionModel competition, MembreModel membre, Integer weight, int overCategory, + public RegisterModel(CompetitionModel competition, MembreModel membre, Float weight, int overCategory, Categorie categorie, ClubModel club) { this.id = new RegisterId(competition.getId(), membre.getId()); this.competition = competition; @@ -91,7 +91,7 @@ public class RegisterModel { return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)]; } - public Integer getWeight2() { + public Float getWeight2() { if (weightReal != null) return weightReal; return weight; 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 e0da444..408ca4d 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/CombEntity.java @@ -27,7 +27,7 @@ public class CombEntity { Genre genre; String country; int overCategory; - Integer weight; + Float weight; List teamMembers; List categoriesInscrites; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java index df37ffe..82f08e2 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java @@ -107,7 +107,7 @@ public class CompetitionData { public static class SimpleRegister { long id; int overCategory; - Integer weight; + Float weight; Categorie categorie; Long club; String club_str; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java index 93d20e5..0cf467a 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -18,8 +18,8 @@ public class RegisterRequestData { private String fname; private String lname; - private Integer weight; - private Integer weightReal; + private Float weight; + private Float weightReal; private Integer overCategory; private boolean lockEdit = false; private List categoriesInscrites; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java index 0dd5799..7f145ee 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java @@ -26,8 +26,8 @@ public class SimpleRegisterComb { private Categorie categorie; private SimpleClubModel club; private Integer licence; - private Integer weight; - private Integer weightReal; + private Float weight; + private Float weightReal; private int overCategory; private boolean hasLicenceActive; private boolean lockEdit; diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java index 54015fd..97d6ee5 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java @@ -86,7 +86,7 @@ public class RTeam { .max(Integer::compareTo) .map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1)); - List s = Stream.concat( + List s = Stream.concat( pair.getKey().stream().map(RegisterModel::getWeight), pair.getValue().stream().map(CompetitionGuestModel::getWeight)) .filter(Objects::nonNull).toList(); @@ -95,7 +95,7 @@ public class RTeam { } else if (s.size() == 1) { team.setWeight(s.get(0)); } else { - team.setWeight((int) s.stream().mapToInt(Integer::intValue) + team.setWeight((float) s.stream().mapToDouble(Float::doubleValue) .average() .orElse(0)); } diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index d018190..e8f6b9d 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -554,11 +554,11 @@ function Modal_({data2, data3, error2, sendRegister, modalState, setModalState,
{t('comp.modal.poids')} {source === "admin" && {t('comp.modal.annoncé')}} - setWeight(e.target.value)}/> {source === "admin" && <>{t('comp.modal.pesé')} - setWeightReal(e.target.value)}/>}
diff --git a/src/main/webapp/src/pages/competition/CompetitionView.jsx b/src/main/webapp/src/pages/competition/CompetitionView.jsx index d055e5b..0caceb9 100644 --- a/src/main/webapp/src/pages/competition/CompetitionView.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionView.jsx @@ -156,11 +156,11 @@ function SelfRegister({data2}) {

{t('comp.monInscription')}