feat: PDF for categories

This commit is contained in:
Thibaut Valentin 2026-02-16 22:43:14 +01:00
parent d857fce71f
commit 752f03cba5
9 changed files with 998 additions and 3 deletions

View File

@ -21,6 +21,7 @@
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jspdf": "^4.1.0",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7",
@ -1605,6 +1606,19 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "19.2.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
@ -1631,6 +1645,13 @@
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz",
"integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA=="
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -1884,6 +1905,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
@ -2028,6 +2059,26 @@
}
]
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
@ -2088,6 +2139,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -2139,6 +2202,16 @@
"node": ">=4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
@ -2384,6 +2457,16 @@
"node": ">=0.4.0"
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -2952,6 +3035,23 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-png/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@ -3348,6 +3448,20 @@
"void-elements": "3.1.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/i18next": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz",
@ -3471,6 +3585,12 @@
"node": ">=12"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -3923,6 +4043,29 @@
"node": ">=6"
}
},
"node_modules/jspdf": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz",
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf/node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@ -4358,6 +4501,13 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -4485,6 +4635,16 @@
"node": ">=6"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
@ -4732,6 +4892,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -4783,6 +4950,16 @@
"node": ">=4"
}
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
@ -5071,6 +5248,16 @@
"node": ">=0.8"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@ -5246,6 +5433,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -5438,6 +5645,16 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uzip": {
"version": "0.20201231.0",
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",

View File

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

View File

@ -5,6 +5,7 @@
"actuel": "Current",
"administration": "Administration",
"adresseDuServeur": "Server address",
"afficher": "Show",
"ajoutAutomatique": "Automatic addition",
"ajouter": "Add",
"ajouterDesCombattants": "Add fighters",
@ -23,6 +24,7 @@
"cartonNoir": "Black card",
"cartonRouge": "Red card",
"catégorie": "Category",
"catégorieDâgeMoyenne": "Middle-aged 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.",
@ -89,6 +91,7 @@
"genre.f": "F",
"genre.h": "M",
"genre.na": "NA",
"imprimer": "Print",
"individuelle": "Individual",
"informationCatégorie": "Category information",
"inscrit": "Registered",

View File

@ -5,6 +5,7 @@
"actuel": "Actuel",
"administration": "Administration",
"adresseDuServeur": "Adresse du serveur",
"afficher": "Afficher",
"ajoutAutomatique": "Ajout automatique",
"ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants",
@ -23,6 +24,7 @@
"cartonNoir": "Carton noir",
"cartonRouge": "Carton rouge",
"catégorie": "Catégorie",
"catégorieDâgeMoyenne": "Catégorie d'âge moyenne",
"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",
@ -89,6 +91,7 @@
"genre.f": "F",
"genre.h": "H",
"genre.na": "NA",
"imprimer": "Imprimer",
"individuelle": "Individuelle",
"informationCatégorie": "Information catégorie",
"inscrit": "Inscrit",

View File

@ -164,3 +164,27 @@ const ProtectionSelector = ({
}
export default ProtectionSelector;
export function getMandatoryProtectionsList(mandatoryProtection, shield, t) {
const protections = [];
const isOn = (bit) => (mandatoryProtection & (1 << (bit - 1))) !== 0;
if (isOn(1)) protections.push(t('casque', {ns: "common"}));
if (isOn(2)) protections.push(t('gorgerin', {ns: "common"}));
if (isOn(3)) protections.push(t('coquilleProtectionPelvienne', {ns: "common"}));
if (isOn(4) && !shield) protections.push(t('gants', {ns: "common"}));
if (isOn(4) && shield) protections.push(t('gantMainsArmées', {ns: "common"}));
if (isOn(5) && shield) protections.push(t('gantMainBouclier', {ns: "common"}));
if (isOn(6)) protections.push(t('plastron', {ns: "common"}));
if (isOn(7) && !shield) protections.push(t('protectionDeBras', {ns: "common"}));
if (isOn(7) && shield) protections.push(t('protectionDeBrasArmé', {ns: "common"}));
if (isOn(8) && shield) protections.push(t('protectionDeBrasDeBouclier', {ns: "common"}));
if (isOn(9)) protections.push(t('protectionDeJambes', {ns: "common"}));
if (isOn(10)) protections.push(t('protectionDeGenoux', {ns: "common"}));
if (isOn(11)) protections.push(t('protectionDeCoudes', {ns: "common"}));
if (isOn(12)) protections.push(t('protectionDorsale', {ns: "common"}));
if (isOn(13)) protections.push(t('protectionDePieds', {ns: "common"}));
return protections;
}

View File

@ -11,7 +11,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
import {faFile, faGlobe, 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 i18n from "i18next";
import {getToastMessage} from "../../../utils/Tools.js";
@ -21,6 +21,7 @@ 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";
const vite_url = import.meta.env.VITE_URL;
@ -177,6 +178,7 @@ function Menu({menuActions, compUuid}) {
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
const teamCardModal = useRef(null);
const printModal = useRef(null);
const {t} = useTranslation("cm");
const [showStateWin, setShowStateWin] = useState(false)
@ -265,7 +267,8 @@ function Menu({menuActions, compUuid}) {
}
const copyScriptToClipboard = () => {
navigator.clipboard.writeText(`<!--suppress ALL -->
// noinspection JSFileReferences
navigator.clipboard.writeText(`
<div id='safca_api_data'></div>
<script type="module">
import {initCompetitionApi} from '${vite_url}/competition.js';
@ -290,6 +293,11 @@ function Menu({menuActions, compUuid}) {
onMouseUp={() => longPressUp("cards")}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t("carton")}/>
<FontAwesomeIcon icon={faPrint} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onClick={() => printModal.current.click()}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('imprimer')}/>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("obs")}
@ -375,6 +383,71 @@ function Menu({menuActions, compUuid}) {
</div>
</div>
</div>
<button ref={printModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#PrintModal"
style={{display: 'none'}}>
Launch printModal
</button>
<div className="modal fade" id="PrintModal" tabIndex="-1" aria-labelledby="PrintModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<PrintModal menuActions={menuActions}/>
</div>
</div>
</div>
</>
}
function PrintModal({menuActions}) {
const [categorie, setCategorie] = useState(true);
const [categorieEmpty, setCategorieEmpty] = useState(false);
const {welcomeData} = useWS();
const {getComb} = useCombs();
const {t} = useTranslation("cm");
const print = (action) => {
const pages = [];
const names = [];
if (categorie) {
if (menuActions.printCategorie) {
const [name, page] = menuActions.printCategorie(categorieEmpty);
pages.push(...page);
names.push(name);
}
}
if (pages.length !== 0) {
makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t)
}
}
return <>
<div className="modal-header">
<h5 className="modal-title">Quoi imprimer ?</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint"
onChange={e => setCategorie(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint">Catégorie sélectionner</label>
</div>
{categorie &&
<div className="form-check" style={{marginLeft: "1em"}}>
<input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2"
onChange={e => setCategorieEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint2">Feuille vierge</label>
</div>}
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("show")}>{t('afficher')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("download")}>{t('enregistrer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("print")}>{t('imprimer')}</button>
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
</div>
</>
}

View File

@ -22,6 +22,7 @@ import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.
import {ScorePanel} from "./ScoreAndCardPanel.jsx";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {AutoCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
import {makePDF} from "../../../utils/cmPdf.jsx";
const vite_url = import.meta.env.VITE_URL;
@ -184,9 +185,28 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
<div className="col-md-9">
{cat && <ListMatch cat={cat} matches={matches} groups={groups} reducer={reducer}/>}
</div>
<PrintMatch menuActions={menuActions} matches={matches} groups={groups} cat={cat}/>
</>
}
function PrintMatch({menuActions, cat, groups, matches}) {
const {cards_v} = useCards();
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
marches2.forEach(m => {
if (m.end && (!m.scores || m.scores.length === 0))
m.scores = [{n_round: 0, s1: 0, s2: 0}];
})
menuActions.printCategorie = (categorieEmpty) => {
return [cat.name, [
{type: "categorie", params: ({cat, matches: marches2, groups, cards_v, categorieEmpty})}
]]
}
return <></>
}
function AddComb({groups, setGroups, removeGroup, menuActions, cat}) {
const {data, setData} = useRequestWS("getRegister", null)
const combDispatch = useCombsDispatch()

View File

@ -1,6 +1,6 @@
import {useEffect, useRef} from "react";
import {scorePrint} from "../../utils/Tools.js";
import {compareCardOrder, useCardsStatic} from "../../hooks/useCard.jsx";
import {useCardsStatic} from "../../hooks/useCard.jsx";
const max_x = 500;
@ -451,3 +451,279 @@ export function DrawGraph({
<canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px", position: "relative", opacity: 1}} id="myCanvas"></canvas>
</div>
}
export function drawGraphForPdf(root = [], size = 14, cards = []) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards);
const sizeY = size * 0.5;
function getBounds(root) {
let px = max_x;
let py;
let maxx, minx, miny, maxy
function drawNode(tree, px, py) {
let death = tree.death() - 1
if (death === 0) {
if (miny > py - sizeY - ((sizeY * 1.5 / 2) | 0)) miny = py - sizeY - (sizeY * 1.5 / 2) | 0;
if (maxy < py + sizeY + ((sizeY * 1.5 / 2) | 0)) maxy = py + sizeY + (sizeY * 1.5 / 2) | 0;
} else {
if (miny > py - sizeY * 2 * death - ((sizeY * 1.5 / 2) | 0))
miny = py - sizeY * 2 * death - ((sizeY * 1.5 / 2) | 0);
if (maxy < py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0))
maxy = py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0);
}
if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8;
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - sizeY * 2 * death);
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + sizeY * 2 * death);
}
if (root != null) {
py = (sizeY * 2 * root.at(0).death() + (((sizeY * 1.5 / 2) | 0) + sizeY) * root.at(0).death()) * 2;
maxx = px;
minx = px;
miny = py - (sizeY * 1.5 / 2) | 0;
maxy = py + (sizeY * 1.5 / 2) | 0;
for (const node of root) {
px = px - size * 2 - size * 8;
if (minx > px) minx = px;
drawNode(node, px, py);
//graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny);
py = maxy + ((sizeY * 2 * node.death() + ((sizeY * 1.5 / 2) | 0)));
px = maxx;
}
} else {
minx = 0;
maxx = 0;
miny = 0;
maxy = 0;
}
return [minx, maxx, miny, maxy];
}
// Fonction pour dessiner du texte avec gestion de la taille
const printText = (ctx, s, x, y, width, height, lineG, lineD) => {
ctx.save();
ctx.translate(x, y);
let tSize = 17;
let ratioX = height * 1.0 / 20.0;
ctx.font = "100 " + tSize + "px Arial";
let mw = width - (ratioX * 2) | 0;
if (ctx.measureText(s).width > mw) {
do {
tSize--;
ctx.font = tSize + "px Arial";
} while (ctx.measureText(s).width > mw && tSize > 10);
if (ctx.measureText(s).width > mw) {
let truncated = "";
const words = s.split(" ");
for (const word of words) {
if (ctx.measureText(truncated + word).width >= mw) {
truncated += "...";
break;
} else {
truncated += word + " ";
}
}
s = truncated;
}
}
const text = ctx.measureText(s);
let dx = (width - text.width) / 2;
let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillText(s, dx, dy, width - dy);
ctx.restore();
ctx.beginPath();
if (lineD) {
ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2);
ctx.lineTo(x + width, y + height / 2);
}
if (lineG) {
ctx.moveTo(x, y + height / 2);
ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2);
}
ctx.stroke();
};
// Fonction pour afficher les scores
const printScores = (ctx, scores, px, py, scale) => {
ctx.save();
ctx.translate(px - size * 2, py - size * scale);
ctx.font = "100 14px Arial";
ctx.textBaseline = 'top';
for (let i = 0; i < scores.length; i++) {
const score = scorePrint(scores[i].s1) + "-" + scorePrint(scores[i].s2);
const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3;
const text = ctx.measureText(score);
let dx = (size * 2 - text.width) / 2;
let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillStyle = '#ffffffdd';
ctx.fillRect(dx, size * 2 * scale / div * i + dy, text.width, 14);
ctx.fillStyle = "#000000";
ctx.fillText(score, dx, size * 2 * scale / div * i + dy, size * 2);
}
ctx.restore();
};
const printCard = (ctx, pos, combId, match) => {
const cards2 = getHeightCardForCombInMatch(combId, match)
if (cards2 != null) {
let oldColor = ctx.fillStyle;
switch (cards2.type) {
case "BLUE":
ctx.fillStyle = "#2e2efd";
break;
case "YELLOW":
ctx.fillStyle = "#d8d800";
break;
case "RED":
ctx.fillStyle = "#FF0000";
break;
case "BLACK":
ctx.fillStyle = "#000000";
break;
default:
ctx.fillStyle = "#FFFFFF00";
}
if (cards2.match === match.id) {
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle
ctx.arc(pos.x + pos.width - 10, pos.y + 5, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = "#000000"
} else
ctx.fillRect(pos.x + pos.width - 18, pos.y - 5, 12, 12);
ctx.fillStyle = oldColor;
}
}
// Fonction pour dessiner un nœud
const drawNode = (ctx, tree, px, py, max_y) => {
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px - size, py);
ctx.stroke();
let death = tree.death() - 1;
let match = tree.data;
if (death === 0) {
ctx.beginPath();
ctx.moveTo(px - size, py + sizeY);
ctx.lineTo(px - size, py - sizeY);
ctx.moveTo(px - size, py + sizeY);
ctx.lineTo(px - size * 2, py + sizeY);
ctx.moveTo(px - size, py - sizeY);
ctx.lineTo(px - size * 2, py - sizeY);
ctx.stroke();
printScores(ctx, match.scores, px, py, 1);
const pos = {x: px - size * 2 - size * 8, y: py - sizeY - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos, match.c1, match)
ctx.fillStyle = "#FF0000"
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true)
const pos2 = {x: px - size * 2 - size * 8, y: py + sizeY - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos2, match.c2, match)
ctx.fillStyle = "#0000FF"
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true)
if (max_y.current < py + sizeY + ((sizeY * 1.5 / 2) | 0)) {
max_y.current = py + sizeY + (sizeY * 1.5 / 2 | 0);
}
} else {
ctx.beginPath();
ctx.moveTo(px - size, py);
ctx.lineTo(px - size, py + sizeY * 2 * death);
ctx.moveTo(px - size, py);
ctx.lineTo(px - size, py - sizeY * 2 * death);
ctx.moveTo(px - size, py + sizeY * 2 * death);
ctx.lineTo(px - size * 2, py + sizeY * 2 * death);
ctx.moveTo(px - size, py - sizeY * 2 * death);
ctx.lineTo(px - size * 2, py - sizeY * 2 * death);
ctx.stroke();
printScores(ctx, match.scores, px, py, 1.5);
const pos = {x: px - size * 2 - size * 8, y: py - sizeY * 2 * death - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos, match.c1, match)
ctx.fillStyle = "#FF0000"
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true)
const pos2 = {x: px - size * 2 - size * 8, y: py + sizeY * 2 * death - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos2, match.c2, match)
ctx.fillStyle = "#0000FF"
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true)
if (max_y.current < py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0)) {
max_y.current = py + sizeY * 2 * death + ((sizeY * 1.5 / 2 | 0));
}
}
if (tree.left != null) {
drawNode(ctx, tree.left, px - size * 2 - size * 8, py - sizeY * 2 * death, max_y);
}
if (tree.right != null) {
drawNode(ctx, tree.right, px - size * 2 - size * 8, py + sizeY * 2 * death, max_y);
}
};
// Dessiner sur le canvas principal
const canvas = document.createElement('canvas');
canvas.id = "myCanvas";
canvas.style.border = "1px solid grey";
canvas.style.marginTop = "10px";
const ctx = canvas.getContext("2d");
const [minx, maxx, miny, maxy] = getBounds(root);
canvas.width = maxx - minx;
canvas.height = maxy - miny;
ctx.translate(-minx, -miny);
ctx.fillStyle = "#000000";
ctx.lineWidth = 2;
ctx.strokeStyle = "#000000";
let px = maxx;
let py;
const max_y = {current: 0};
py = (sizeY * 2 * root[0].death() + (((sizeY * 1.5 / 2) | 0) + sizeY) * root[0].death()) * 2;
max_y.current = py + (sizeY * 1.5 / 2 | 0);
for (const node of root) {
let win_name = "";
if (node.data.end) {
win_name = node.data.win > 0
? (node.data.c1FullName === null ? "???" : node.data.c1FullName)
: (node.data.c2FullName === null ? "???" : node.data.c2FullName);
}
ctx.fillStyle = "#18A918";
printText(ctx, win_name,
px - size * 2 - size * 8, py - ((sizeY * 1.5 / 2) | 0),
size * 8, (sizeY * 1.5 | 0), true, false);
px = px - size * 2 - size * 8;
drawNode(ctx, node, px, py, max_y);
py = max_y.current + ((sizeY * 2 * node.death() + ((sizeY * 1.5 / 2) | 0)));
px = maxx;
}
return canvas;
}

View File

@ -0,0 +1,378 @@
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>}
</>
}