From ed5d73c25fb9564f000b13cfb4d293e0a6929a4e Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 17 Feb 2026 21:35:29 +0100 Subject: [PATCH] feat: improve generation PDF for categories --- src/main/webapp/package-lock.json | 10 + src/main/webapp/package.json | 1 + src/main/webapp/public/locales/en/cm.json | 8 + src/main/webapp/public/locales/fr/cm.json | 8 + .../src/components/cm/ListPresetSelect.jsx | 2 +- .../src/pages/competition/editor/CMAdmin.jsx | 192 +++++++-- src/main/webapp/src/utils/Tools.js | 26 ++ src/main/webapp/src/utils/cmPdf.js | 358 +++++++++++++++++ src/main/webapp/src/utils/cmPdf.jsx | 378 ------------------ 9 files changed, 581 insertions(+), 402 deletions(-) create mode 100644 src/main/webapp/src/utils/cmPdf.js delete mode 100644 src/main/webapp/src/utils/cmPdf.jsx diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 6366724..e9606cb 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -22,6 +22,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "jspdf": "^4.1.0", + "jspdf-autotable": "^5.0.7", "jszip": "^3.10.1", "leaflet": "^1.9.4", "obs-websocket-js": "^5.0.7", @@ -4060,6 +4061,15 @@ "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": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 1fde546..fd64a9f 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -24,6 +24,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "jspdf": "^4.1.0", + "jspdf-autotable": "^5.0.7", "jszip": "^3.10.1", "leaflet": "^1.9.4", "obs-websocket-js": "^5.0.7", diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index 8a640d9..1c2f09c 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -25,6 +25,7 @@ "cartonRouge": "Red card", "catégorie": "Category", "catégorieDâgeMoyenne": "Middle-aged category", + "catégorieSélectionnée": "Selected 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.", @@ -86,6 +87,7 @@ "etatDesTablesDeMarque": "State of marque tables", "exporter": "Export", "fermer": "Close", + "feuilleVierge": "Blank sheet", "finalesUniquement": "Finals only", "genre": "Gender", "genre.f": "F", @@ -121,6 +123,7 @@ "poule": "Pool", "poulePour": "Pool for: ", "préparation...": "Preparing...", + "quoiImprimer?": "What print?", "remplacer": "Replace", "rouge": "Red", "réinitialiser": "Reset", @@ -164,6 +167,9 @@ "toast.matchs.create.error": "Error while creating matches.", "toast.matchs.create.pending": "Creating matches in progress...", "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.pending": "Updating team...", "toast.team.update.success": "Team updated!", @@ -183,6 +189,8 @@ "tournois": "Tournaments", "tousLesMatchs": "All matches", "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.scripte": "Copy integration script", "ttm.table.inverserLaPosition": "Reverse fighter positions on this screen", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 16356a5..95d4100 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -25,6 +25,7 @@ "cartonRouge": "Carton rouge", "catégorie": "Catégorie", "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", "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", @@ -86,6 +87,7 @@ "etatDesTablesDeMarque": "Etat des tables de marque", "exporter": "Exporter", "fermer": "Fermer", + "feuilleVierge": "Feuille vierge", "finalesUniquement": "Finales uniquement", "genre": "Genre", "genre.f": "F", @@ -121,6 +123,7 @@ "poule": "Poule", "poulePour": "Poule pour: ", "préparation...": "Préparation...", + "quoiImprimer?": "Quoi imprimer ?", "remplacer": "Remplacer", "rouge": "Rouge", "réinitialiser": "Réinitialiser", @@ -164,6 +167,9 @@ "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.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.pending": "Mise à jour de l'équipe...", "toast.team.update.success": "Équipe mise à jour !", @@ -183,6 +189,8 @@ "tournois": "Tournois", "tousLesMatchs": "Tous les matchs", "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.scripte": "Copier le scripte d'intégration", "ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran", diff --git a/src/main/webapp/src/components/cm/ListPresetSelect.jsx b/src/main/webapp/src/components/cm/ListPresetSelect.jsx index 6212e53..7877f0d 100644 --- a/src/main/webapp/src/components/cm/ListPresetSelect.jsx +++ b/src/main/webapp/src/components/cm/ListPresetSelect.jsx @@ -15,7 +15,7 @@ export function ListPresetSelect({disabled, value, onChange, returnId = true}) { value={returnId ? value : (value ? value.id : -1)} onChange={e => { if (returnId) { - onChange(e.target.value) + onChange(Number(e.target.value)) } else { onChange(data.find(c => c.id === Number(e.target.value))) } diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index ee21e74..e8c92ed 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -2,7 +2,7 @@ import React, {useEffect, useId, useRef, useState} from "react"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; 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 {CategoryContent} from "./CategoryAdminContent.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 {Trans, useTranslation} from "react-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 {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, 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; @@ -52,7 +52,7 @@ export function CMAdmin({compUuid}) { return <>
- +
@@ -399,46 +399,102 @@ function Menu({menuActions, compUuid}) { } function PrintModal({menuActions}) { - const [categorie, setCategorie] = useState(true); + const [categorie, setCategorie] = 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 {getComb} = useCombs(); const {t} = useTranslation("cm"); const print = (action) => { - const pages = []; - const names = []; + const pagesPromise = []; - if (categorie) { - if (menuActions.printCategorie) { - const [name, page] = menuActions.printCategorie(categorieEmpty); - pages.push(...page); - names.push(name); - } - } + if (categorie && menuActions.printCategorie) + pagesPromise.push(menuActions.printCategorie(categorieEmpty)) - if (pages.length !== 0) { - makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t) - } + 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 names = []; + let errors = 0; + + for (const result of results) { + if (result.status === "fulfilled") { + const [name, page, error] = result.value; + pages.push(...page); + 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) { + makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t, logo) + } + }) + }), getToastMessage("toast.print", "cm")) } return <>
-
Quoi imprimer ?
+
{t('quoiImprimer?')}
setCategorie(e.target.checked)}/> - +
{categorie &&
setCategorieEmpty(e.target.checked)}/> - + +
} + +
+ setPreset(e.target.checked)}/> + +
+ {preset &&
+
+ setPresetEmpty(e.target.checked)}/> + +
+ +
} + +
+ setAllCat(e.target.checked)}/> + +
+ {allCat && +
+ setAllCatEmpty(e.target.checked)}/> +
}
@@ -574,9 +630,7 @@ function TeamCardModal() { } -function CategoryHeader({ - cat, setCatId - }) { +function CategoryHeader({cat, setCatId, menuActions}) { const setLoading = useLoadingSwitcher() const bthRef = useRef(null); const newBthRef = useRef(null); @@ -722,9 +776,101 @@ function CategoryHeader({
+ } +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}) { const id = useId() const [name, setName] = useState("") diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 7f4f78e..9eec7de 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -418,3 +418,29 @@ export function hex2rgb(hex) { // 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; + } + }); +} diff --git a/src/main/webapp/src/utils/cmPdf.js b/src/main/webapp/src/utils/cmPdf.js new file mode 100644 index 0000000..af0a0b9 --- /dev/null +++ b/src/main/webapp/src/utils/cmPdf.js @@ -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) + } +} diff --git a/src/main/webapp/src/utils/cmPdf.jsx b/src/main/webapp/src/utils/cmPdf.jsx deleted file mode 100644 index 4c8f4fe..0000000 --- a/src/main/webapp/src/utils/cmPdf.jsx +++ /dev/null @@ -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 <>  - 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 - - - {!classement && } - {!classement && } - {!classement && } - - - - - - - - - {matches.map((m, index) => ( - - {!classement && } - {!classement && } - {!classement && } - - - - - - - ))} - -
{t('no')}{t('poule')}{t('zone')}{t('rouge')}{t('résultat')}{t('blue')}
{index + 1}{m.poule}{liceName[index % liceName.length]}{m.end && ((m.win > 0 && "X") || (m.win === 0 && "-"))}{scoreToString2(m, cards_v)}{m.end && ((m.win < 0 && "X") || (m.win === 0 && "-"))}
-} - -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
- Arbre - {cat.fullClassement && - 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}/>} -
-} - -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 - - - {Object.keys(groups2).map((poule) => )} - - - - - {Object.keys(groups2).map((poule) => )} - - - -
{t('poule')} {poule}
{groups2[poule].map(o => o.name).join(", ")}
-} - -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( -
- {processPage(pages[0], ({pdf_name: name, c_name, getComb, t}))} -
); - - 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 - 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 <> - - - - - - - - - - - - - - - - -
- - - - - - - -
- Logo - -

{c_name}

-

{cat.name}

-
-
- - - - - - - - - - - - - -
{t('catégorieDâgeMoyenne')} : {getCatName(catAverage)} - {t('arme', {ns: 'common'})} : {getSwordTypeName(cat.preset?.sword)} - {t('taille')} {getSwordSize(cat.preset?.sword, catAverage, genreAverage)} - - {t('bouclier', {ns: 'common'})} : {getShieldTypeName(cat.preset?.shield)} - {t('taille')} {getShieldSize(cat.preset?.shield, catAverage)} -
{t('duréeRound')} : {timePrint(time.round)}{t('duréePause')} : {timePrint(time.pause)}{t('nombreDeCombattants')} : {nbComb}
-
- {t('protectionObligatoire', {ns: 'common'})} {getMandatoryProtectionsList(CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ? - cat.preset?.mandatoryProtection1 : cat.preset?.mandatoryProtection2, cat.preset?.shield, t).join(", ") || "---"} -
- -
- -
- {(cat.type & 1) === 1 && -
- 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}/> -
} - - {(cat.type & 2) === 2 && -
- {cat.treeAreClassement ? t('classement') : t('tournois')} : - -
} - -}