feat: add podium PDF
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m28s

This commit is contained in:
Thibaut Valentin 2026-02-19 15:55:46 +01:00
parent ed5d73c25f
commit 2f390b03e2
10 changed files with 238 additions and 5 deletions

View File

@ -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();
}

View File

@ -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<CardModel> cards,
public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> 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 {

View File

@ -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;
}

View File

@ -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<List<PodiumEntity>> getPodium(WebSocketConnection connection, Object o) {
List<CardModel> 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<CategoryModel, List<MatchModel>> 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<ResultCategoryData.ClassementData> podium) {
}
}

View File

@ -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

View File

@ -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é
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

View File

@ -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",

View File

@ -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",

View File

@ -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}) {
<label className="form-check-label" htmlFor="checkPrint6">{t('feuilleVierge')}</label>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={podium} id="checkPrint7"
onChange={e => setPodium(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint7">Podium</label>
</div>
{podium &&
<div style={{marginLeft: "1em"}}>
<label htmlFor="range3" className="form-label">{t('jusquauRang')} {podiumRank} </label>
<input type="range" className="form-range" min="1" max="20" step="1" id="range3" value={podiumRank}
onChange={e => setPodiumRank(Number(e.target.value))}/>
</div>}
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("show")}>{t('afficher')}</button>

View File

@ -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);
}