feat: improve generation PDF for categories

This commit is contained in:
Thibaut Valentin 2026-02-17 21:35:29 +01:00
parent 0a368454c4
commit ed5d73c25f
9 changed files with 581 additions and 402 deletions

View File

@ -22,6 +22,7 @@
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"jspdf": "^4.1.0", "jspdf": "^4.1.0",
"jspdf-autotable": "^5.0.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7", "obs-websocket-js": "^5.0.7",
@ -4060,6 +4061,15 @@
"html2canvas": "^1.0.0-rc.5" "html2canvas": "^1.0.0-rc.5"
} }
}, },
"node_modules/jspdf-autotable": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz",
"integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3 || ^4"
}
},
"node_modules/jspdf/node_modules/fflate": { "node_modules/jspdf/node_modules/fflate": {
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",

View File

@ -24,6 +24,7 @@
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"jspdf": "^4.1.0", "jspdf": "^4.1.0",
"jspdf-autotable": "^5.0.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7", "obs-websocket-js": "^5.0.7",

View File

@ -25,6 +25,7 @@
"cartonRouge": "Red card", "cartonRouge": "Red card",
"catégorie": "Category", "catégorie": "Category",
"catégorieDâgeMoyenne": "Middle-aged category", "catégorieDâgeMoyenne": "Middle-aged category",
"catégorieSélectionnée": "Selected category",
"catégoriesVontêtreCréées": "weight categories will be created", "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?", "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.", "certainsCombattantsNontPasDePoidsRenseigné": "Some fighters do not have a weight listed; they will NOT be included in the categories.",
@ -86,6 +87,7 @@
"etatDesTablesDeMarque": "State of marque tables", "etatDesTablesDeMarque": "State of marque tables",
"exporter": "Export", "exporter": "Export",
"fermer": "Close", "fermer": "Close",
"feuilleVierge": "Blank sheet",
"finalesUniquement": "Finals only", "finalesUniquement": "Finals only",
"genre": "Gender", "genre": "Gender",
"genre.f": "F", "genre.f": "F",
@ -121,6 +123,7 @@
"poule": "Pool", "poule": "Pool",
"poulePour": "Pool for: ", "poulePour": "Pool for: ",
"préparation...": "Preparing...", "préparation...": "Preparing...",
"quoiImprimer?": "What print?",
"remplacer": "Replace", "remplacer": "Replace",
"rouge": "Red", "rouge": "Red",
"réinitialiser": "Reset", "réinitialiser": "Reset",
@ -164,6 +167,9 @@
"toast.matchs.create.error": "Error while creating matches.", "toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...", "toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.", "toast.matchs.create.success": "Matches created successfully.",
"toast.print.error": "Error while preparing print",
"toast.print.pending": "Preparing print...",
"toast.print.success": "Print ready!",
"toast.team.update.error": "Error while updating team", "toast.team.update.error": "Error while updating team",
"toast.team.update.pending": "Updating team...", "toast.team.update.pending": "Updating team...",
"toast.team.update.success": "Team updated!", "toast.team.update.success": "Team updated!",
@ -183,6 +189,8 @@
"tournois": "Tournaments", "tournois": "Tournaments",
"tousLesMatchs": "All matches", "tousLesMatchs": "All matches",
"toutConserver": "Keep all", "toutConserver": "Keep all",
"touteLaCatégorie": "The entire category",
"toutesLesCatégories": "All categories",
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
"ttm.admin.scripte": "Copy integration script", "ttm.admin.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen", "ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",

View File

@ -25,6 +25,7 @@
"cartonRouge": "Carton rouge", "cartonRouge": "Carton rouge",
"catégorie": "Catégorie", "catégorie": "Catégorie",
"catégorieDâgeMoyenne": "Catégorie d'âge moyenne", "catégorieDâgeMoyenne": "Catégorie d'âge moyenne",
"catégorieSélectionnée": "Catégorie sélectionnée",
"catégoriesVontêtreCréées": "catégories de poids vont être créées", "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 ?", "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", "certainsCombattantsNontPasDePoidsRenseigné": "Certains combattants n'ont pas de poids renseigné, ils ne seront PAS insert dans les catégories",
@ -86,6 +87,7 @@
"etatDesTablesDeMarque": "Etat des tables de marque", "etatDesTablesDeMarque": "Etat des tables de marque",
"exporter": "Exporter", "exporter": "Exporter",
"fermer": "Fermer", "fermer": "Fermer",
"feuilleVierge": "Feuille vierge",
"finalesUniquement": "Finales uniquement", "finalesUniquement": "Finales uniquement",
"genre": "Genre", "genre": "Genre",
"genre.f": "F", "genre.f": "F",
@ -121,6 +123,7 @@
"poule": "Poule", "poule": "Poule",
"poulePour": "Poule pour: ", "poulePour": "Poule pour: ",
"préparation...": "Préparation...", "préparation...": "Préparation...",
"quoiImprimer?": "Quoi imprimer ?",
"remplacer": "Remplacer", "remplacer": "Remplacer",
"rouge": "Rouge", "rouge": "Rouge",
"réinitialiser": "Réinitialiser", "réinitialiser": "Réinitialiser",
@ -164,6 +167,9 @@
"toast.matchs.create.error": "Erreur lors de la création des matchs.", "toast.matchs.create.error": "Erreur lors de la création des matchs.",
"toast.matchs.create.pending": "Création des matchs en cours...", "toast.matchs.create.pending": "Création des matchs en cours...",
"toast.matchs.create.success": "Matchs créés avec succès.", "toast.matchs.create.success": "Matchs créés avec succès.",
"toast.print.error": "Erreur lors de la génération du PDF",
"toast.print.pending": "Génération du PDF en cours...",
"toast.print.success": "PDF généré !",
"toast.team.update.error": "Erreur lors de la mise à jour de l'équipe", "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.pending": "Mise à jour de l'équipe...",
"toast.team.update.success": "Équipe mise à jour !", "toast.team.update.success": "Équipe mise à jour !",
@ -183,6 +189,8 @@
"tournois": "Tournois", "tournois": "Tournois",
"tousLesMatchs": "Tous les matchs", "tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver", "toutConserver": "Tout conserver",
"touteLaCatégorie": "Toute la catégorie",
"toutesLesCatégories": "Toutes les catégories",
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
"ttm.admin.scripte": "Copier le scripte d'intégration", "ttm.admin.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran", "ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",

View File

@ -15,7 +15,7 @@ export function ListPresetSelect({disabled, value, onChange, returnId = true}) {
value={returnId ? value : (value ? value.id : -1)} value={returnId ? value : (value ? value.id : -1)}
onChange={e => { onChange={e => {
if (returnId) { if (returnId) {
onChange(e.target.value) onChange(Number(e.target.value))
} else { } else {
onChange(data.find(c => c.id === Number(e.target.value))) onChange(data.find(c => c.id === Number(e.target.value)))
} }

View File

@ -2,7 +2,7 @@ import React, {useEffect, useId, useRef, useState} from "react";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {build_tree, resize_tree} from "../../../utils/TreeUtils.js" import {build_tree, from_sendTree, resize_tree} from "../../../utils/TreeUtils.js"
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {CategoryContent} from "./CategoryAdminContent.jsx"; import {CategoryContent} from "./CategoryAdminContent.jsx";
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx"; import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
@ -14,14 +14,14 @@ import {detectOptimalBackground} from "../../../components/SmartLogoBackground.j
import {faFile, faGlobe, faPrint, faTableCellsLarge, faTrash} from "@fortawesome/free-solid-svg-icons"; import {faFile, faGlobe, faPrint, faTableCellsLarge, faTrash} from "@fortawesome/free-solid-svg-icons";
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import i18n from "i18next"; import i18n from "i18next";
import {getToastMessage} from "../../../utils/Tools.js"; import {getToastMessage, toDataURL, win_end} from "../../../utils/Tools.js";
import {copyStyles} from "../../../utils/copyStyles.js"; import {copyStyles} from "../../../utils/copyStyles.js";
import {StateWindow} from "./StateWindow.jsx"; import {StateWindow} from "./StateWindow.jsx";
import {CombName, useCombs} from "../../../hooks/useComb.jsx"; import {CombName, useCombs} from "../../../hooks/useComb.jsx";
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx";
import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
import {makePDF} from "../../../utils/cmPdf.jsx"; import {makePDF} from "../../../utils/cmPdf.js";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -52,7 +52,7 @@ export function CMAdmin({compUuid}) {
return <> return <>
<div className="card"> <div className="card">
<div className='card-header'> <div className='card-header'>
<CategoryHeader cat={cat} setCatId={setCatId}/> <CategoryHeader cat={cat} setCatId={setCatId} menuActions={menuActions}/>
</div> </div>
<div className="card-body"> <div className="card-body">
@ -399,46 +399,102 @@ function Menu({menuActions, compUuid}) {
} }
function PrintModal({menuActions}) { function PrintModal({menuActions}) {
const [categorie, setCategorie] = useState(true); const [categorie, setCategorie] = useState(false);
const [categorieEmpty, setCategorieEmpty] = useState(false); const [categorieEmpty, setCategorieEmpty] = useState(false);
const [preset, setPreset] = useState(false);
const [presetEmpty, setPresetEmpty] = useState(false);
const [allCat, setAllCat] = useState(false);
const [allCatEmpty, setAllCatEmpty] = useState(false);
const [presetSelect, setPresetSelect] = useState(-1)
const {welcomeData} = useWS(); const {welcomeData} = useWS();
const {getComb} = useCombs(); const {getComb} = useCombs();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const print = (action) => { const print = (action) => {
const pagesPromise = [];
if (categorie && menuActions.printCategorie)
pagesPromise.push(menuActions.printCategorie(categorieEmpty))
if (preset && menuActions.printCategoriePreset)
pagesPromise.push(menuActions.printCategoriePreset(presetEmpty, presetSelect))
if (allCat && menuActions.printAllCategorie)
pagesPromise.push(menuActions.printAllCategorie(categorieEmpty, welcomeData?.name + " - " + t('toutesLesCatégories')))
toast.promise(
toDataURL("/Logo-FFSAF-2023.png").then(logo => {
return Promise.allSettled(pagesPromise).then(results => {
const pages = []; const pages = [];
const names = []; const names = [];
let errors = 0;
if (categorie) { for (const result of results) {
if (menuActions.printCategorie) { if (result.status === "fulfilled") {
const [name, page] = menuActions.printCategorie(categorieEmpty); const [name, page, error] = result.value;
pages.push(...page); pages.push(...page);
names.push(name); names.push(name);
errors += error
} else if (result.status === "rejected") {
errors += 1;
} }
} }
if (errors > 0) {
toast.error(t('erreurGénérationPages', {count: errors}));
}
if (pages.length !== 0) { if (pages.length !== 0) {
makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t) makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t, logo)
} }
})
}), getToastMessage("toast.print", "cm"))
} }
return <> return <>
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">Quoi imprimer ?</h5> <h5 className="modal-title">{t('quoiImprimer?')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint" <input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint"
onChange={e => setCategorie(e.target.checked)}/> onChange={e => setCategorie(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint">Catégorie sélectionner</label> <label className="form-check-label" htmlFor="checkPrint">{t('catégorieSélectionnée')}</label>
</div> </div>
{categorie && {categorie &&
<div className="form-check" style={{marginLeft: "1em"}}> <div className="form-check" style={{marginLeft: "1em"}}>
<input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2" <input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2"
onChange={e => setCategorieEmpty(e.target.checked)}/> onChange={e => setCategorieEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint2">Feuille vierge</label> <label className="form-check-label" htmlFor="checkPrint2">{t('feuilleVierge')}</label>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={preset} id="checkPrint3"
onChange={e => setPreset(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint3">{t('touteLaCatégorie')}</label>
</div>
{preset && <div style={{marginLeft: "1em"}}>
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={presetEmpty} id="checkPrint4"
onChange={e => setPresetEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint4">{t('feuilleVierge')}</label>
</div>
<ListPresetSelect value={presetSelect} onChange={setPresetSelect}/>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={allCat} id="checkPrint5"
onChange={e => setAllCat(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint5">{t('toutesLesCatégories')}</label>
</div>
{allCat &&
<div className="form-check" style={{marginLeft: "1em"}}>
<input className="form-check-input" type="checkbox" checked={allCatEmpty} id="checkPrint6"
onChange={e => setAllCatEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint6">{t('feuilleVierge')}</label>
</div>} </div>}
</div> </div>
@ -574,9 +630,7 @@ function TeamCardModal() {
</> </>
} }
function CategoryHeader({ function CategoryHeader({cat, setCatId, menuActions}) {
cat, setCatId
}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const bthRef = useRef(null); const bthRef = useRef(null);
const newBthRef = useRef(null); const newBthRef = useRef(null);
@ -722,9 +776,101 @@ function CategoryHeader({
</div> </div>
</div> </div>
</div> </div>
<PrintCats menuActions={menuActions} cats={cats}/>
</div> </div>
} }
function PrintCats({menuActions, cats}) {
const {cards_v} = useCards();
const {sendRequest} = useWS();
function readAndConvertMatch(matches, data) {
matches.push({
...data,
c1: data.c1?.id,
c2: data.c2?.id,
c1_cacheName: data.c1?.fname + " " + data.c1?.lname,
c2_cacheName: data.c2?.fname + " " + data.c2?.lname
})
}
const run = (categorieEmpty, cats2, name = "") => {
const pagesPromise = cats2.sort((a, b) => a.name.localeCompare(b.name)).map(cat_ => {
return sendRequest('getFullCategory', cat_.id)
.then((data) => {
const cat = {
id: data.id,
name: data.name,
liceName: data.liceName,
type: data.type,
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
raw_trees: data.trees.sort((a, b) => a.level - b.level),
treeAreClassement: data.treeAreClassement,
fullClassement: data.fullClassement,
preset: data.preset,
}
if (name === "") {
name = data.preset.name;
}
const newCards = {};
for (const o of data.cards)
newCards[o.id] = o
let matches2 = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_));
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_));
const activeMatches = matches2.filter(m => m.poule !== '-')
const groups = matches2.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i)
.map(d => {
let poule = activeMatches.find(m => (m.c1 === d || m.c2 === d) && m.categorie_ord !== -42)?.poule
if (!poule)
poule = '-'
return {id: d, poule: poule}
})
matches2 = matches2.filter(m => m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
matches2.forEach(m => {
if (m.end && (!m.scores || m.scores.length === 0))
m.scores = [{n_round: 0, s1: 0, s2: 0}];
})
return {
type: "categorie",
params: ({cat, matches: matches2, groups, cards_v: Object.values({...cards_v, ...newCards}), categorieEmpty})
}
})
})
return Promise.allSettled(pagesPromise)
.then((results) => {
const pages = [];
let error = 0;
for (const result of results) {
if (result.status === "fulfilled") {
pages.push(result.value);
} else {
console.error(result.error);
error++;
}
}
return [name, pages, error];
})
}
menuActions.printCategoriePreset = (categorieEmpty, preset) => {
return run(categorieEmpty, cats.filter(cat => cat.preset?.id === preset))
}
menuActions.printAllCategorie = (categorieEmpty, name) => {
return run(categorieEmpty, cats, name)
}
}
function ModalContent({state, setCatId, setConfirm, confirmRef}) { function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const id = useId() const id = useId()
const [name, setName] = useState("") const [name, setName] = useState("")

View File

@ -418,3 +418,29 @@ export function hex2rgb(hex) {
// return {r, g, b} // return {r, g, b}
return {r, g, b}; return {r, g, b};
} }
export function toDataURL(src, outputFormat) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
const canvas = document.createElement('CANVAS');
const ctx = canvas.getContext('2d');
let dataURL;
canvas.height = this.naturalHeight;
canvas.width = this.naturalWidth;
ctx.drawImage(this, 0, 0);
dataURL = canvas.toDataURL(outputFormat);
resolve(dataURL);
};
img.onerror = function() {
reject(new Error('Could not load image at ' + src));
}
img.src = src;
if (img.complete || img.complete === undefined) {
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
img.src = src;
}
});
}

View File

@ -0,0 +1,358 @@
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 {getMandatoryProtectionsList} from "../components/ProtectionSelector.jsx";
import {scoreToString2} from "./CompetitionTools.js";
import {TreeNode} from "./TreeUtils.js";
import {drawGraphForPdf} from "../pages/result/DrawGraph.jsx";
import autoTable from 'jspdf-autotable'
function combName(getComb, combId) {
if (!combId)
return " "
const comb = getComb(combId, null);
if (comb) {
if (comb.lname === "__team")
return `${comb.fname}`
return `${comb.fname} ${comb.lname}`
} else {
return `[Comb #${combId}]`
}
}
export function makePDF(action, pagesList, name, c_name, getComb, t, logo) {
//https://github.com/parallax/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L59
const doc = new jsPDF('p', 'pt', 'a4');
doc.setProperties({title: name, author: "FFSAF - Intranet", subject: c_name + " - " + name, creator: "FFSAF - Intranet"});
for (let i = 0; i < pagesList.length; i++) {
const context = {pdf_doc: doc, pdf_name: name, c_name, getComb, t, ...pagesList[i].params, logo}
switch (pagesList[i].type) {
case "categorie":
generateCategoriePDF(context);
break;
default:
break
}
if (i !== pagesList.length - 1)
doc.addPage();
}
switch (action) {
case "show":
window.open(doc.output('bloburl', {filename: name + '.pdf'}));
break;
case "print":
const iframe = document.createElement('iframe'); //load content in an iframe to print later
document.body.appendChild(iframe);
iframe.style.display = 'none';
iframe.src = doc.output('bloburl', {filename: name + '.pdf'});
iframe.onload = function () {
setTimeout(function () {
iframe.focus();
iframe.contentWindow.print();
}, 1);
};
break;
case "download":
default:
doc.save(name + '.pdf');
break;
}
}
function matchList(pdf_doc, matches, cat, cards_v, classement, getComb, t) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards_v);
const liceName = (cat.liceName || "N/A").split(";");
const getBG = (combId, match) => {
const c = getHeightCardForCombInMatch(combId, match)
if (!c)
return ""
let bg = "";
let text = "#000";
switch (c.type) {
case "YELLOW":
bg = "#ffc107";
break;
case "RED":
bg = "#dc3545";
text = "#FFF";
break;
case "BLACK":
bg = "#000000";
text = "#FFF";
break;
case "BLUE":
bg = "#0d6efd";
text = "#FFF";
break;
}
return {fillColor: bg, textColor: text}
}
const head = [
...(classement ? [
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
] : [
{content: t('no'), styles: {halign: "center"}},
{content: t('poule'), styles: {halign: "center"}},
{content: t('zone'), styles: {halign: "center"}},
]),
{content: "", styles: {halign: "center"}},
{content: t('rouge'), styles: {halign: "center"}},
{content: t('résultat'), styles: {halign: "center"}},
{content: t('blue'), styles: {halign: "center"}},
{content: "", styles: {halign: "center"}},
]
const body = matches.map((m, index) => ([
...(classement ? [
{content: `${m.categorie_ord + 1}-${m.categorie_ord + 2}`, styles: {halign: "center", padding: "2pt 0"}}
] : [
{content: index + 1, styles: {halign: "center", padding: "2pt 0"}},
{content: m.poule, styles: {halign: "center"}},
{content: liceName[index % liceName.length], styles: {halign: "center"}},
]),
{content: m.end ? (m.win > 0 ? "X" : (m.win === 0 ? "-" : "")) : " ", styles: {halign: "center", ...getBG(m.c1, m)}},
{content: combName(getComb, m.c1), styles: {halign: "center", minWidth: "11em", paddingLeft: "0.2em"}},
{content: scoreToString2(m, cards_v), styles: {halign: "center"}},
{content: combName(getComb, m.c2), styles: {halign: "center", minWidth: "11em", paddingRight: "0.2em"}},
{content: m.end ? (m.win < 0 ? "X" : (m.win === 0 ? "-" : "")) : " ", styles: {halign: "center", ...getBG(m.c2, m)}},
]))
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
columnStyles: classement ? {
0: {cellWidth: 35},
1: {cellWidth: 15},
2: {cellWidth: "auto"},
3: {cellWidth: 110},
4: {cellWidth: "auto"},
5: {cellWidth: 15},
} : {
0: {cellWidth: 20},
1: {cellWidth: 35},
2: {cellWidth: 30},
3: {cellWidth: 15},
4: {cellWidth: "auto"},
5: {cellWidth: 110},
6: {cellWidth: "auto"},
7: {cellWidth: 15},
},
head: [head],
body: body,
theme: 'grid',
})
}
function buildTree(pdf_doc, treeData, treeRaw, matches, cat, cards_v, getComb, t, categorieEmpty, comb_count = 0) {
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = categorieEmpty ? null : getComb(matchData?.c1)
const c2 = categorieEmpty ? null : getComb(matchData?.c2)
const scores2 = []
for (const score of matchData?.scores) {
scores2.push({
...score,
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
})
}
let node = new TreeNode({
...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
return node
}
let out = []
for (let i = 0; i < treeRaw.length; i++) {
if (treeRaw.at(i).level > -10) {
out.push(parseTree(treeData.at(i)))
}
}
const canvas = drawGraphForPdf(out, 24, cards_v)
const imgData = canvas.toDataURL('image/png');
const height = canvas.height * (pdf_doc.internal.pageSize.getWidth() - 80) / canvas.width;
pdf_doc.addImage(imgData, 'PNG', 40, pdf_doc.lastAutoTable.finalY, pdf_doc.internal.pageSize.getWidth() - 80, height);
pdf_doc.lastAutoTable.finalY += height;
if (cat.fullClassement) {
let size = 0;
if (out.length > 0)
size = out[0].getMaxChildrenAtDepth(out[0].death() - 1)
const matches2 = cat.raw_trees?.filter(n => n.level <= -10).reverse().map(d => categorieEmpty ? ({}) : matches.find(m => m.id === d.match?.id))
if (matches2.length === 0) {
while (Math.ceil(comb_count / 2) - size > matches2.length)
matches2.push({})
}
matches2.forEach((v, i) => {
v.categorie_ord = (i + size) * 2
})
matchList(pdf_doc, matches2, cat, cards_v, true, getComb, t)
}
}
function pouleList(pdf_doc, groups, getComb, t) {
const groups2 = groups.map(g => {
const comb = getComb(g.id);
return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
}).sort((a, b) => {
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;
if (!acc[poule]) {
acc[poule] = [];
}
acc[poule].push(curr);
return acc;
}, {});
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
head: [Object.keys(groups2).map((poule) => ({content: `${t('poule')} ${poule}`, styles: {halign: "center"}}))],
body: [Object.keys(groups2).map((poule) => ({
content: groups2[poule].map(o => o.name).join(", "),
styles: {halign: "center", valign: "middle"}
}))],
theme: 'grid',
})
}
function makeHeader(pdf_doc, c_name, p_name, logo) {
autoTable(pdf_doc, {
startY: 20,
body: [[
{content: "", src: `${logo}`, rowSpan: 2, styles: {halign: 'center', valign: 'middle'}},
{content: c_name, colSpan: 3, styles: {halign: "center", valign: 'middle', fontSize: 20}},
], [
{content: p_name, colSpan: 3, styles: {halign: "center", valign: 'middle', fontSize: 20}}
]],
theme: 'plain',
columnStyles: {
0: {cellWidth: 60, minCellHeight: 60},
},
didDrawCell: function (data) {
if (data.column.index === 0 && data.row.index === 0) {
const dim = data.cell.height - data.cell.padding('vertical');
const textPos = data.cell;
pdf_doc.addImage(data.cell.raw.src, textPos.x, textPos.y + data.cell.padding('vertical') / 2, dim, dim);
}
}
})
}
function generateCategoriePDF({pdf_doc, cat, matches, groups, getComb, cards_v, c_name, categorieEmpty, t, logo}) {
let catAverage = "---";
let genreAverage = "H";
let nbComb = 0;
let time = 0;
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => categorieEmpty ? ({...m, end: false, scores: []}) : m)
if (marches2.length !== 0) {
const genres = [];
const cats = [];
const combs_ = [];
for (const m of marches2) {
if (m.c1 && !combs_.includes(m.c1))
combs_.push(m.c1);
if (m.c2 && !combs_.includes(m.c2))
combs_.push(m.c2);
}
combs_.map(cId => getComb(cId, null)).filter(c => c && c.categorie)
.forEach(c => {
cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory))
genres.push(c.genre)
});
const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length);
nbComb = combs_.length;
catAverage = CatList.at(catAvg) || "---";
genreAverage = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length) > 0.5 ? "F" : "H";
if (cat.preset && cat.preset.categories) {
const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie));
let p;
if (catAvailable.includes(catAvg)) {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg);
} else {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) ===
catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a));
}
time = {round: p.roundDuration, pause: p.pauseDuration}
}
}
makeHeader(pdf_doc, c_name, cat.name, logo)
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY,
styles: {fontSize: 10, cellPadding: 3},
body: [[
{content: `${t('catégorieDâgeMoyenne')} : ${getCatName(catAverage)}`, styles: {halign: "left"}},
{
content: `${t('arme', {ns: 'common'})} : ${getSwordTypeName(cat.preset?.sword)} - ${t('taille')} ${getSwordSize(cat.preset?.sword, catAverage, genreAverage)}`,
styles: {halign: "left"}
},
{
content: `${t('bouclier', {ns: 'common'})} : ${getShieldTypeName(cat.preset?.shield)} - ${t('taille')} ${getShieldSize(cat.preset?.shield, catAverage)}`,
styles: {halign: "left"}
},
], [
{content: `${t('duréeRound')} : ${timePrint(time.round)}`, styles: {halign: "left"}},
{content: `${t('duréePause')} : ${timePrint(time.pause)}`, styles: {halign: "left"}},
{content: `${t('nombreDeCombattants')} : ${nbComb}`, styles: {halign: "left"}},
], [
{
content: `${t('protectionObligatoire', {ns: 'common'})} ${getMandatoryProtectionsList(CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
cat.preset?.mandatoryProtection1 : cat.preset?.mandatoryProtection2, cat.preset?.shield, t).join(", ") || "---"}`,
colSpan: 3,
styles: {halign: "left"}
},
]],
theme: 'grid',
})
pouleList(pdf_doc, groups, getComb, t)
if ((cat.type & 1) === 1)
matchList(pdf_doc, marches2.filter(m => m.categorie_ord !== -42).sort((a, b) => a.categorie_ord - b.categorie_ord),
cat, categorieEmpty ? [] : cards_v, false, getComb, t)
if ((cat.type & 2) === 2) {
pdf_doc.setFontSize(12);
pdf_doc.text((cat.treeAreClassement ? t('classement') : t('tournois')) + ':', 40, pdf_doc.lastAutoTable.finalY + 16)
pdf_doc.lastAutoTable.finalY += 16;
buildTree(pdf_doc, cat.trees, cat.raw_trees, marches2, cat, categorieEmpty ? [] : cards_v, getComb, t, categorieEmpty, nbComb)
}
}

View File

@ -1,378 +0,0 @@
import {jsPDF} from 'jspdf'
import {renderToString} from "react-dom/server";
import React from "react";
import {hasEffectCard, useCardsStatic} from "../hooks/useCard.jsx";
import {CatList, getCatName, getShieldSize, getShieldTypeName, getSwordSize, getSwordTypeName, timePrint, virtualScore, win_end} from "./Tools.js";
import {getMandatoryProtectionsList} from "../components/ProtectionSelector.jsx";
import {scoreToString2} from "./CompetitionTools.js";
import {TreeNode} from "./TreeUtils.js";
import {drawGraphForPdf} from "../pages/result/DrawGraph.jsx";
function CombName({getComb, combId}) {
if (!combId)
return <>&#8195;</>
const comb = getComb(combId, null);
if (comb) {
if (comb.lname === "__team")
return <>{comb.fname}</>
return <>{comb.fname} {comb.lname}</>
} else {
return <>[Comb #{combId}]</>
}
}
function MatchList({matches, cat, cards_v, classement, getComb, t}) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards_v);
const liceName = (cat.liceName || "N/A").split(";");
const getBG = (combId, match, cat) => {
const c = getHeightCardForCombInMatch(combId, match)
if (!c)
return ""
let bg = "";
let text = "#000";
switch (c.type) {
case "YELLOW":
bg = "#ffc107";
break;
case "RED":
bg = "#dc3545";
text = "#FFF";
break;
case "BLACK":
bg = "#000000";
text = "#FFF";
break;
case "BLUE":
bg = "#0d6efd";
text = "#FFF";
break;
}
return {
backgroundColor: bg,
color: text,
borderRadius: c.match === match.id ? "50%" : "0",
opacity: hasEffectCard(c, match.id, cat.id) ? 1 : 0.5
}
}
return <table style={{width: "100%", borderCollapse: "collapse"}} border={1}>
<thead>
<tr>
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{t('no')}</th>}
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{t('poule')}</th>}
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{t('zone')}</th>}
<th></th>
<th style={{textAlign: "center"}}>{t('rouge')}</th>
<th style={{textAlign: "center"}}>{t('résultat')}</th>
<th style={{textAlign: "center"}}>{t('blue')}</th>
<th></th>
</tr>
</thead>
<tbody>
{matches.map((m, index) => (
<tr key={m.id} id={m.id} style={{
background: index % 2 ? "#ededed" : "white",
borderLeft: "0.5px solid gray",
borderTop: index === 0 ? "2px solid black" : ""
}}>
{!classement && <th style={{textAlign: "center", padding: "2pt 0"}} scope="row">{index + 1}</th>}
{!classement && <td style={{textAlign: "center"}}>{m.poule}</td>}
{!classement && <td style={{textAlign: "center"}}>{liceName[index % liceName.length]}</td>}
<td style={{textAlign: "center", ...getBG(m.c1, m, cat)}}>{m.end && ((m.win > 0 && "X") || (m.win === 0 && "-"))}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}><CombName combId={m.c1} getComb={getComb}/></td>
<td style={{textAlign: "center"}}>{scoreToString2(m, cards_v)}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}><CombName combId={m.c2} getComb={getComb}/></td>
<td style={{textAlign: "center", ...getBG(m.c2, m, cat)}}>{m.end && ((m.win < 0 && "X") || (m.win === 0 && "-"))}</td>
</tr>
))}
</tbody>
</table>
}
function BuildTree({treeData, treeRaw, matches, cat, cards_v, getComb, t, categorieEmpty}) {
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = categorieEmpty ? null : getComb(matchData?.c1)
const c2 = categorieEmpty ? null : getComb(matchData?.c2)
const scores2 = []
for (const score of matchData?.scores) {
scores2.push({
...score,
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
})
}
let node = new TreeNode({
...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
return node
}
function initTree(data_in, data_raw) {
let out = []
for (let i = 0; i < data_raw.length; i++) {
if (data_raw.at(i).level > -10) {
out.push(parseTree(data_in.at(i)))
}
}
return out
}
const imgData = drawGraphForPdf(initTree(treeData, treeRaw), 24, cards_v).toDataURL('image/png');
return <div>
<img src={imgData} alt="Arbre" style={{width: "100%", margin: "4pt 0", objectFit: "scale-down"}}/>
{cat.fullClassement &&
<MatchList
matches={cat.raw_trees?.filter(n => n.level <= -10).reverse().map(d => categorieEmpty ? ({}) : matches.find(m => m.id === d.match?.id))}
cat={cat}
cards_v={cards_v} classement={true} t={t} getComb={getComb}/>}
</div>
}
function PouleList({groups, getComb, t}) {
const groups2 = groups.map(g => {
const comb = getComb(g.id);
return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
}).sort((a, b) => {
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;
if (!acc[poule]) {
acc[poule] = [];
}
acc[poule].push(curr);
return acc;
}, {});
return <table style={{width: "100%"}}>
<thead style={{borderCollapse: "collapse"}}>
<tr style={{borderCollapse: "collapse"}}>
{Object.keys(groups2).map((poule) => <th key={poule} style={{
textAlign: "center",
borderCollapse: "collapse",
border: "1px solid black"
}}>{t('poule')} {poule}</th>)}
</tr>
</thead>
<tbody style={{borderCollapse: "collapse"}}>
<tr style={{borderCollapse: "collapse"}}>
{Object.keys(groups2).map((poule) => <td key={poule} style={{
textAlign: "center",
borderCollapse: "collapse",
border: "1px solid black"
}}>{groups2[poule].map(o => o.name).join(", ")}</td>)}
</tr>
</tbody>
</table>
}
export function makePDF(action, pagesList, name, c_name, getComb, t) {
//https://github.com/parallax/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L59
const doc = new jsPDF('p', 'pt', 'a4');
let pageHeight = doc.internal.pageSize.getHeight();
htmlPage(doc, pagesList);
function htmlPage(doc2, pages, index = 0) {
const hasPages = pages.length > 0;
if (!hasPages) {
doc2.setProperties({title: name, author: "FFSAF - Intranet", subject: c_name + " - " + name, creator: "FFSAF - Intranet"});
switch (action) {
case "show":
window.open(doc.output('bloburl', {filename: name + '.pdf'}));
break;
case "print":
const iframe = document.createElement('iframe'); //load content in an iframe to print later
document.body.appendChild(iframe);
iframe.style.display = 'none';
iframe.src = doc.output('bloburl', {filename: name + '.pdf'});
iframe.onload = function () {
setTimeout(function () {
iframe.focus();
iframe.contentWindow.print();
}, 1);
};
break;
case "download":
default:
doc.save(name + '.pdf');
break;
}
return;
}
let htmlElement = renderToString(
<div style={{width: "835.82px", margin: "0 auto", letterSpacing: "0.065em"}}>
{processPage(pages[0], ({pdf_name: name, c_name, getComb, t}))}
</div>);
return doc2.html(htmlElement, {
y: index * pageHeight,
callback: function (pdf) {
if (index > 0 && pages.length > 1) pdf.addPage("a4", "p");
const restPages = pages.slice();
restPages.splice(0, 1);
htmlPage(pdf, restPages, index + 1);
},
html2canvas: {
scale: 0.65,
logging: false,
letterRendering: 1,
allowTaint: true,
useCORS: true,
},
margin: [30, 30, 30, 30],
width: 595.28,
//windowWidth: 300
});
}
}
function processPage(page, context) {
switch (page.type) {
case "categorie":
return <GenerateCategoriePDF {...context} {...page.params} />
default:
return <></>
}
}
export function GenerateCategoriePDF({cat, matches, groups, getComb, cards_v, c_name, categorieEmpty, t}) {
let catAverage = "---";
let genreAverage = "H";
let nbComb = 0;
let time = 0;
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => categorieEmpty ? ({...m, end: false, scores: []}) : m)
if (marches2.length !== 0) {
const genres = [];
const cats = [];
const combs_ = [];
for (const m of marches2) {
if (m.c1 && !combs_.includes(m.c1))
combs_.push(m.c1);
if (m.c2 && !combs_.includes(m.c2))
combs_.push(m.c2);
}
combs_.map(cId => getComb(cId, null)).filter(c => c && c.categorie)
.forEach(c => {
cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory))
genres.push(c.genre)
});
const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length);
nbComb = combs_.length;
catAverage = CatList.at(catAvg) || "---";
genreAverage = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length) > 0.5 ? "F" : "H";
if (cat.preset && cat.preset.categories) {
const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie));
let p;
if (catAvailable.includes(catAvg)) {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg);
} else {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) ===
catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a));
}
time = {round: p.roundDuration, pause: p.pauseDuration}
}
}
return <>
<table style={{width: "100%", borderCollapse: "collapse", border: "1px solid black"}}>
<thead>
<tr>
<td>
<table style={{width: "100%"}}>
<tbody>
<tr>
<td style={{width: "90pt"}}>
<img src="/Logo-FFSAF-2023.png" alt="Logo" style={{height: "90pt", width: "90pt"}}/>
</td>
<td align={'center'}>
<h2 style={{marginLeft: "10pt", textAlign: "center"}}>{c_name}</h2>
<h2 style={{marginLeft: "10pt", textAlign: "center"}}>{cat.name}</h2>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</thead>
<tbody className="table-group-divider">
<tr>
<td>
<table style={{width: "100%"}}>
<tbody>
<tr>
<td><strong>{t('catégorieDâgeMoyenne')} :</strong> {getCatName(catAverage)}</td>
<td>
<strong>{t('arme', {ns: 'common'})} :</strong> {getSwordTypeName(cat.preset?.sword)} - {t('taille')} {getSwordSize(cat.preset?.sword, catAverage, genreAverage)}
</td>
<td>
<strong>{t('bouclier', {ns: 'common'})} :</strong> {getShieldTypeName(cat.preset?.shield)} - {t('taille')} {getShieldSize(cat.preset?.shield, catAverage)}
</td>
</tr>
<tr>
<td><strong>{t('duréeRound')} :</strong> {timePrint(time.round)}</td>
<td><strong>{t('duréePause')} :</strong> {timePrint(time.pause)}</td>
<td><strong>{t('nombreDeCombattants')} :</strong> {nbComb}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<strong>{t('protectionObligatoire', {ns: 'common'})}</strong> {getMandatoryProtectionsList(CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
cat.preset?.mandatoryProtection1 : cat.preset?.mandatoryProtection2, cat.preset?.shield, t).join(", ") || "---"}
</td>
</tr>
</tfoot>
</table>
<div style={{marginTop: "10pt"}}>
<PouleList groups={groups} t={t} getComb={getComb}/>
</div>
{(cat.type & 1) === 1 &&
<div style={{marginTop: "7pt"}}>
<MatchList matches={marches2.filter(m => m.categorie_ord !== -42).sort((a, b) => a.categorie_ord - b.categorie_ord)} cat={cat}
cards_v={categorieEmpty ? [] : cards_v} classement={false} t={t} getComb={getComb}/>
</div>}
{(cat.type & 2) === 2 &&
<div style={{marginTop: "7pt"}}>
<strong>{cat.treeAreClassement ? t('classement') : t('tournois')} :</strong>
<BuildTree treeData={cat.trees} treeRaw={cat.raw_trees} cat={cat} matches={marches2} cards_v={categorieEmpty ? [] : cards_v}
getComb={getComb} t={t} categorieEmpty={categorieEmpty}/>
</div>}
</>
}