From 2f390b03e203dce4724cd71ba417f0ac71d9fe28 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 19 Feb 2026 15:55:46 +0100 Subject: [PATCH] feat: add podium PDF --- .../ffsaf/data/model/CombModel.java | 2 + .../ffsaf/domain/service/ResultService.java | 5 +- .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 4 + .../fr/titionfire/ffsaf/ws/recv/RPDF.java | 107 ++++++++++++++++++ .../resources/lang/messages_en.properties | 3 + .../resources/lang/messages_fr.properties | 5 +- src/main/webapp/public/locales/en/cm.json | 2 + src/main/webapp/public/locales/fr/cm.json | 2 + .../src/pages/competition/editor/CMAdmin.jsx | 26 ++++- src/main/webapp/src/utils/cmPdf.js | 87 +++++++++++++- 10 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/recv/RPDF.java diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CombModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CombModel.java index f8fe1f7..a409206 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CombModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CombModel.java @@ -1,9 +1,11 @@ package fr.titionfire.ffsaf.data.model; +import fr.titionfire.ffsaf.utils.Categorie; import fr.titionfire.ffsaf.utils.ResultPrivacy; public interface CombModel { Long getCombId(); String getName(); String getName(MembreModel model, ResultPrivacy privacy); + Categorie getCategorie(); } 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 c3d4fbd..a9cc7bd 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -188,6 +188,7 @@ public class ResultService { comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w, stat.pointMake, stat.pointTake, stat.getPointRate()); }) + .filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0) .sorted(Comparator .comparing(ResultCategoryData.RankArray::getScore) .thenComparing(ResultCategoryData.RankArray::getWin) @@ -212,7 +213,7 @@ public class ResultService { }); } - private void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List cards, + public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List cards, ResultCategoryData out) { if ((categoryModel.getType() & 2) != 0) { AtomicInteger rank = new AtomicInteger(0); @@ -258,7 +259,7 @@ public class ResultService { .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(), m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); out.getClassement() - .add(new ResultCategoryData.ClassementData(rank.getAndIncrement(), m.getC2(), + .add(new ResultCategoryData.ClassementData(rank.get(), m.getC2(), m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); } } else { diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index 56d8f42..7edb0ad 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -54,6 +54,9 @@ public class CompetitionWS { @Inject RState rState; + @Inject + RPDF rpdf; + @Inject SecurityCtx securityCtx; @@ -99,6 +102,7 @@ public class CompetitionWS { getWSReceiverMethods(RCard.class, rCard); getWSReceiverMethods(RTeam.class, rTeam); getWSReceiverMethods(RState.class, rState); + getWSReceiverMethods(RPDF.class, rpdf); executor = notifyExecutor; } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RPDF.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RPDF.java new file mode 100644 index 0000000..7aa1a17 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RPDF.java @@ -0,0 +1,107 @@ +package fr.titionfire.ffsaf.ws.recv; + +import fr.titionfire.ffsaf.data.model.CardModel; +import fr.titionfire.ffsaf.data.model.CategoryModel; +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.data.repository.CardRepository; +import fr.titionfire.ffsaf.data.repository.CategoryRepository; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.domain.entity.MatchModelExtend; +import fr.titionfire.ffsaf.domain.service.ResultService; +import fr.titionfire.ffsaf.domain.service.TradService; +import fr.titionfire.ffsaf.rest.data.ResultCategoryData; +import fr.titionfire.ffsaf.utils.Categorie; +import fr.titionfire.ffsaf.ws.PermLevel; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + +@WithSession +@ApplicationScoped +@RegisterForReflection +public class RPDF { + + @Inject + CompetitionRepository competitionRepository; + + @Inject + MatchRepository matchRepository; + + @Inject + CardRepository cardRepository; + + @Inject + CategoryRepository categoryRepository; + + @Inject + ResultService resultService; + + @Inject + TradService trad; + + @Transactional + @WSReceiver(code = "getPodium", permission = PermLevel.VIEW) + public Uni> getPodium(WebSocketConnection connection, Object o) { + List cards = new java.util.ArrayList<>(); + + return cardRepository.list("competition.uuid = ?1", connection.pathParam("uuid")) + .invoke(cards::addAll) + .chain(__ -> matchRepository.list("category.compet.uuid = ?1", connection.pathParam("uuid"))) + .chain(matchs -> { + HashMap> map = new HashMap<>(); + for (MatchModel match : matchs) { + if (!map.containsKey(match.getCategory())) + map.put(match.getCategory(), new java.util.ArrayList<>()); + map.get(match.getCategory()).add(match); + } + + return Multi.createFrom().iterable(map.entrySet()) + .onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree())) + .map(entry -> { + ResultCategoryData tmp = new ResultCategoryData(); + + double cmoy = entry.getValue().stream().flatMap(m -> Stream.of(m.getC1(), m.getC2())) + .filter(c -> c != null && c.getCategorie() != null) + .mapToInt(c -> c.getCategorie().ordinal()) + .average().orElse(0); + Categorie categorie_moy = Categorie.values()[(int) Math.ceil(cmoy)]; + + resultService.getArray2( + entry.getValue().stream().map(m -> new MatchModelExtend(m, cards)).toList(), + null, tmp); + resultService.getClassementArray(entry.getKey(), null, cards, tmp); + + String source = ""; + if ((entry.getKey().getType() & 2) != 0) { + if (entry.getKey().isTreeAreClassement()) + source = trad.t("podium.source.classement", connection); + else + source = trad.t("podium.source.tree", connection); + } else if ((entry.getKey().getType() & 1) != 0) + source = trad.t("podium.source.poule", connection); + + + return new PodiumEntity(entry.getKey().getName(), source, categorie_moy, + tmp.getClassement()); + }) + .collect().asList(); + }); + } + + @RegisterForReflection + public static record PodiumEntity(String poule_name, String source, Categorie categorie, + List podium) { + + } +} diff --git a/src/main/resources/lang/messages_en.properties b/src/main/resources/lang/messages_en.properties index 14ee869..4d9bfc6 100644 --- a/src/main/resources/lang/messages_en.properties +++ b/src/main/resources/lang/messages_en.properties @@ -89,3 +89,6 @@ carton.non.trouver=Card not found card.cannot.be.added=Unable to add the card configuration.non.supportee=Unsupported configuration err.match.termine=Error, a placement match has already been played +podium.source.classement=Ranking +podium.source.tree=Tournaments +podium.source.poule=Pool diff --git a/src/main/resources/lang/messages_fr.properties b/src/main/resources/lang/messages_fr.properties index ac73a7f..e869324 100644 --- a/src/main/resources/lang/messages_fr.properties +++ b/src/main/resources/lang/messages_fr.properties @@ -84,4 +84,7 @@ demande.d.affiliation.non.trouve=Demande d'affiliation introuvable carton.non.trouver=Carton introuvable card.cannot.be.added=Impossible d'ajouter le carton configuration.non.supportee=Configuration non supportée -err.match.termine=Erreur, un match de classement a déjà été joué \ No newline at end of file +err.match.termine=Erreur, un match de classement a déjà été joué +podium.source.classement=Classement +podium.source.tree=Tournois +podium.source.poule=Poule \ No newline at end of file diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index 1c2f09c..db892dc 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -97,6 +97,7 @@ "individuelle": "Individual", "informationCatégorie": "Category information", "inscrit": "Registered", + "jusquauRang": "Up to the rank", "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", @@ -143,6 +144,7 @@ "select.sélectionnerDesCombatants": "Select fighters", "select.à": "to", "serveur": "Server", + "source": "Source", "suivant": "Next", "supprimer": "Delete", "supprimerUn": "Delete one", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 95d4100..86a559f 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -97,6 +97,7 @@ "individuelle": "Individuelle", "informationCatégorie": "Information catégorie", "inscrit": "Inscrit", + "jusquauRang": "Jusqu'au rang", "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", @@ -143,6 +144,7 @@ "select.sélectionnerDesCombatants": "Sélectionner des combatants", "select.à": "à", "serveur": "Serveur", + "source": "Source", "suivant": "Suivant", "supprimer": "Supprimer", "supprimerUn": "Supprimer un", diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index e8c92ed..8ee1395 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -405,13 +405,23 @@ function PrintModal({menuActions}) { const [presetEmpty, setPresetEmpty] = useState(false); const [allCat, setAllCat] = useState(false); const [allCatEmpty, setAllCatEmpty] = useState(false); + const [podium, setPodium] = useState(false); + const [podiumRank, setPodiumRank] = useState(4); const [presetSelect, setPresetSelect] = useState(-1) - const {welcomeData} = useWS(); + const {sendRequest, welcomeData} = useWS(); const {getComb} = useCombs(); const {t} = useTranslation("cm"); + const podiumPromise = (podiumRank_) => { + return sendRequest("getPodium", {}).then(data => { + return [welcomeData?.name + " - " + "Podium", [ + {type: "podium", params: ({data, maxRank: podiumRank_, minRank: Math.min(4, podiumRank_)})}, + ]]; + }); + } + const print = (action) => { const pagesPromise = []; @@ -424,6 +434,9 @@ function PrintModal({menuActions}) { if (allCat && menuActions.printAllCategorie) pagesPromise.push(menuActions.printAllCategorie(categorieEmpty, welcomeData?.name + " - " + t('toutesLesCatégories'))) + if (podium) + pagesPromise.push(podiumPromise(podiumRank)); + toast.promise( toDataURL("/Logo-FFSAF-2023.png").then(logo => { return Promise.allSettled(pagesPromise).then(results => { @@ -497,6 +510,17 @@ function PrintModal({menuActions}) { } +
+ setPodium(e.target.checked)}/> + +
+ {podium && +
+ + setPodiumRank(Number(e.target.value))}/> +
}
diff --git a/src/main/webapp/src/utils/cmPdf.js b/src/main/webapp/src/utils/cmPdf.js index af0a0b9..767b952 100644 --- a/src/main/webapp/src/utils/cmPdf.js +++ b/src/main/webapp/src/utils/cmPdf.js @@ -1,6 +1,17 @@ import {jsPDF} from 'jspdf' import {useCardsStatic} from "../hooks/useCard.jsx"; -import {CatList, getCatName, getShieldSize, getShieldTypeName, getSwordSize, getSwordTypeName, timePrint, virtualScore, win_end} from "./Tools.js"; +import { + CatList, + getCatName, + getShieldSize, + getShieldTypeName, + getSwordSize, + getSwordTypeName, + sortCategories, + timePrint, + virtualScore, + win_end +} from "./Tools.js"; import {getMandatoryProtectionsList} from "../components/ProtectionSelector.jsx"; import {scoreToString2} from "./CompetitionTools.js"; import {TreeNode} from "./TreeUtils.js"; @@ -31,6 +42,9 @@ export function makePDF(action, pagesList, name, c_name, getComb, t, logo) { case "categorie": generateCategoriePDF(context); break; + case "podium": + generatePodium(context); + break; default: break } @@ -356,3 +370,74 @@ function generateCategoriePDF({pdf_doc, cat, matches, groups, getComb, cards_v, buildTree(pdf_doc, cat.trees, cat.raw_trees, marches2, cat, categorieEmpty ? [] : cards_v, getComb, t, categorieEmpty, nbComb) } } + +function generatePodium({pdf_doc, data, t, logo, c_name, minRank = 4, maxRank = 4}) { + makeHeader(pdf_doc, c_name, "Podium", logo) + + const data2 = data.sort((a, b) => { + let tmp = sortCategories(a.categorie, b.categorie); + if (tmp !== 0) + return tmp; + return a.poule_name.localeCompare(b.poule_name) + }) + + let finalY2 = pdf_doc.lastAutoTable.finalY; + let finalY3 = pdf_doc.lastAutoTable.finalY; + for (let i = 0; i < data2.length; i++) { + const p = data2[i]; + let pageNumber = pdf_doc.internal.getNumberOfPages() + + const body = Array.from({length: minRank}, () => []); + for (const c of p.podium) { + if (c.rank > maxRank) + continue; + + if (body[c.rank - 1]) + body[c.rank - 1].push(c.name) + else + body[c.rank - 1] = [c.name] + } + for (let j = 0; j < body.length; j++) { + if (body[j].length === 0) + body[j].push(" ") + body[j] = [ + {content: j + 1, styles: {halign: "center"}}, + {content: body[j].join(", "), styles: {halign: "center"}} + ] + } + + autoTable(pdf_doc, { + startY: finalY3 + 7, + margin: i % 2 ? {left: pdf_doc.internal.pageSize.getWidth() / 2 + 7} : {right: pdf_doc.internal.pageSize.getWidth() / 2 + 7}, + styles: {fontSize: 10, cellPadding: 3}, + columnStyles: { + 0: {cellWidth: 35}, + 1: {cellWidth: "auto"}, + }, + pageBreak: "avoid", + showHead: 'firstPage', + head: [[ + {content: p.poule_name, colSpan: 2, styles: {halign: "center"}}, + ], [ + {content: t('place', {ns: "result"}), styles: {halign: "center"}}, + {content: t('combattants', {ns: 'result'}), styles: {halign: "center"}}, + ]], + body: body, + foot: [[ + {content: t('source') + " : " + p.source, colSpan: 2, styles: {halign: "left", fontSize: 8}}, + ]], + rowPageBreak: 'auto', + theme: 'grid', + }) + + if (i % 2 === 0) { + finalY2 = pdf_doc.lastAutoTable.finalY; + if (pageNumber !== pdf_doc.internal.getNumberOfPages()) + finalY3 = 33 + } else { + pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY); + finalY3 = pdf_doc.lastAutoTable.finalY; + } + } + pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY); +}