ffsaf-site/src/main/webapp/src/components/cm/AutoCatModalContent.jsx

592 lines
27 KiB
JavaScript

import React, {useEffect, useState} from "react";
import {Trans, useTranslation} from "react-i18next";
import {useCountries} from "../../hooks/useCountries.jsx";
import {ListPresetSelect} from "./ListPresetSelect.jsx";
import {CatList, getCatName} from "../../utils/Tools.js";
import {useCombs} from "../../hooks/useComb.jsx";
import {toast} from "react-toastify";
import {build_tree} from "../../utils/TreeUtils.js";
import {createMatch} from "../../utils/CompetitionTools.js";
import {useWS} from "../../hooks/useWS.jsx";
export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) {
const country = useCountries('fr')
const {t} = useTranslation("cm");
const [country_, setCountry_] = useState("")
const [gender, setGender] = useState({H: true, F: true, NA: true})
const [cat, setCat] = useState([])
const [weightMin, setWeightMin] = useState(0)
const [weightMax, setWeightMax] = useState(0)
const [team, setTeam] = useState(false)
const [preset, setPreset] = useState(-1)
useEffect(() => {
setPreset(defaultPreset)
}, [defaultPreset])
const setCat_ = (e, index) => {
if (e.target.checked) {
if (!cat.includes(index)) {
setCat([...cat, index])
}
} else {
setCat(cat.filter(c => c !== index))
}
}
function applyFilter(dataIn, dataOut) {
dataIn.forEach(comb_ => {
const comb = data.find(d => d.id === comb_.id);
if (comb == null)
return;
if ((country_ === "" || comb.country === country_)
&& (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
&& (cat.includes(Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)))
&& (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin)
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)
&& ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team)
&& (preset === -1 || comb.categoriesInscrites.includes(preset))) {
dataOut.push(comb)
}
}
)
}
const dispoFiltered = [];
if (data != null)
applyFilter(data, dispoFiltered);
const handleSubmit = (e) => {
e.preventDefault();
const toReplace = makePoule(dispoFiltered, groups);
setGroups(prev => [...prev.filter(g => !toReplace.some(r => r.id === g.id)), ...toReplace]);
}
const handleReplace = (e) => {
e.preventDefault();
const toReplace = makePoule(dispoFiltered, []);
setGroups(prev => [...prev.map(g => ({id: g.id, poule: "-"})).filter(g => !toReplace.some(r => r.id === g.id)), ...toReplace]);
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="autoCatModalLabel">{t('ajoutAutomatique')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="d-flex flex-wrap justify-content-around mb-1">
<div style={{width: "12em"}}>
<label htmlFor="inputState0" className="form-label">{t('pays')}</label>
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
<option value={""}>{t('--Tous--')}</option>
{country && Object.keys(country).sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
}).map((key, _) => {
return (<option key={key} value={key}>{country[key]}</option>)
})}
</select>
</div>
<ListPresetSelect value={preset} onChange={setPreset}/>
</div>
<div className="d-flex flex-wrap justify-content-around mb-3">
<div>
<label className="form-label">{t('genre')}</label>
<div className="d-flex align-items-center">
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
onChange={e => setGender((prev) => {
return {...prev, H: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
</div>
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
onChange={e => setGender((prev) => {
return {...prev, F: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
onChange={e => setGender((prev) => {
return {...prev, NA: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
</div>
</div>
</div>
<div>
<label className="form-label">{t('team')}</label>
<div className="d-flex align-items-center">
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck" checked={team}
onChange={e => setTeam(e.target.checked)}/>
<label className="form-check-label" htmlFor="gridCheck">{t('team')}</label>
</div>
</div>
</div>
<div>
<label htmlFor="input5" className="form-label">{t('poids')}</label>
<div className="row-cols-sm-auto d-flex align-items-center">
<div style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0"
name="999"
onChange={e => setWeightMin(Number(e.target.value))}/></div>
<div><span>{t('select.à')}</span></div>
<div style={{width: "4.25em"}}><input type="number" className="form-control" value={weightMax} min="0" name="999"
onChange={e => setWeightMax(Number(e.target.value))}/></div>
<div><small>{t('select.msg1')}</small></div>
</div>
</div>
</div>
<div className="d-flex flex-wrap justify-content-around mb-1">
<div className="d-flex flex-wrap mb-3">
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
{t('catégorie')} :
</label>
{CatList.map((cat_, index) => {
return <div key={index} className="input-group"
style={{display: "contents"}}>
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox"
id={"categoriesInput" + index} checked={cat.includes(index)} aria-label={getCatName(cat_)}
onChange={e => setCat_(e, index)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{getCatName(cat_)}</label>
</div>
</div>
})}
</div>
</div>
<span>{dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')} {dispoFiltered.length > 10 &&
<span style={{color: "red"}}>{t('uneCatégorieNePeutContenirPlusDe10Combattants')}</span>}</span>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}
disabled={dispoFiltered.length <= 0 || dispoFiltered.length > 10}>{t('ajouter')}</button>
<button type="submit" className="btn btn-warning" data-bs-dismiss="modal" onClick={handleReplace}
disabled={dispoFiltered.length <= 0 || dispoFiltered.length > 10}>{t('remplacer')}</button>
</div>
</>
}
function makePoule(combIn, groups) {
combIn = combIn.sort(() => Math.random() - 0.5);
const maxInPoule = Math.ceil(combIn.length / 2);
const out = []
const pa = [];
const pb = [];
let nameA;
let nameB;
groups.forEach(g => {
const existsInCombIn = combIn.some(c => c.id === g.id);
if (existsInCombIn) {
if ((pa.length === 0 || g.poule === nameA) && pa.length < maxInPoule) {
nameA = g.poule || "1";
pa.push(g.id);
} else if ((pb.length === 0 || g.poule === nameB) && pb.length < maxInPoule) {
if (!(nameA === (g.poule || (nameA === "1" ? "2" : "1")))) {
nameB = g.poule || (nameA === "1" ? "2" : "1");
pb.push(g.id);
}
}
}
});
nameA = nameA || (nameB === "1" ? "2" : "1");
nameB = nameB || (nameA === "1" ? "2" : "1");
if (combIn.length <= 5) {
combIn.forEach(c => {
if (!pa.includes(c.id))
pa.push(c.id)
});
} else {
for (const c of combIn) {
if (pa.includes(c.id) || pb.includes(c.id))
continue;
const club = c.club_str || (c.teamMembers && c.teamMembers[0].club_str) || "";
const countInPa = pa.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
const countInPb = pb.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
if (pa.length < maxInPoule && (countInPa <= countInPb || pb.length >= maxInPoule)) {
pa.push(c.id);
} else if (pb.length < maxInPoule) {
pb.push(c.id);
} else {
pa.push(c.id);
}
}
}
pa.forEach(id => out.push({id: id, poule: nameA}));
pb.forEach(id => out.push({id: id, poule: nameB}));
return out;
}
function makeWeightCategories(combs) {
combs = combs.filter(c => c.weight != null).sort((a, b) => a.weight - b.weight); // Add random for same weight ?
const catCount = Math.ceil(combs.length / 10);
const catSize = combs.length / catCount;
const catMaxSize = Math.min(Math.ceil(catSize), 10);
const catMinSize = Math.max(Math.floor(catSize), 3); // Add marge ?
const categories = Array.from({length: catCount}, () => []);
for (let i = 0; i < combs.length; i++) {
categories[Math.floor(i / catSize)].push(combs[i]);
}
let change = false;
let maxIterations = 500;
do {
change = false;
// ------ move in upper direction if better and possible ------
let needFree = -1;
let dIfFree = 0;
for (let i = 0; i < catCount - 1; i++) {
const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight;
const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight;
if (weightDiff > nextWeightDiff && categories.at(i).length > catMinSize) {
if (categories.at(i + 1).length < catMaxSize) {
const movedComb = categories.at(i).pop();
categories.at(i + 1).unshift(movedComb);
change = true;
} else if (weightDiff - nextWeightDiff > dIfFree) {
needFree = i;
dIfFree = weightDiff - nextWeightDiff;
}
}
}
if (needFree !== -1) {
let haveSpace = -1;
let maxDiff = 0;
for (let i = needFree + 1; i < catCount; i++) {
if (categories.at(i).length < catMaxSize) {
haveSpace = i;
break;
}
}
if (haveSpace !== -1) {
for (let i = needFree + 1; i < haveSpace; i++) {
const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight;
const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight;
const diffIfFree = weightDiff - nextWeightDiff;
if (diffIfFree > maxDiff) {
maxDiff = diffIfFree;
}
}
if (maxDiff < dIfFree) {
for (let i = needFree; i < haveSpace; i++) {
const movedComb = categories.at(i).pop();
categories.at(i + 1).unshift(movedComb);
change = true;
}
}
}
}
// ------ move in lower direction if better and possible ------
needFree = -1;
dIfFree = 0;
for (let i = 1; i < catCount; i++) {
const currentFirst = categories[i][0];
const currentSecondFirst = categories[i][1];
const prevLast = categories[i - 1][categories[i - 1].length - 1];
const weightDiff = currentSecondFirst.weight - currentFirst.weight;
const prevWeightDiff = currentFirst.weight - prevLast.weight;
if (weightDiff > prevWeightDiff && categories.at(i).length > catMinSize) {
if (categories.at(i - 1).length < catMaxSize) {
const movedComb = categories.at(i).shift();
categories.at(i - 1).push(movedComb);
change = true;
} else if (weightDiff - prevWeightDiff > dIfFree) {
needFree = i;
dIfFree = weightDiff - prevWeightDiff;
}
}
}
if (needFree !== -1) {
let haveSpace = -1;
let maxDiff = 0;
for (let i = needFree - 1; i >= 0; i--) {
if (categories.at(i).length < catMaxSize) {
haveSpace = i;
break;
}
}
if (haveSpace !== -1) {
for (let i = needFree - 1; i > haveSpace; i--) {
const currentFirst = categories[i][0];
const currentSecondFirst = categories[i][1];
const prevLast = categories[i - 1][categories[i - 1].length - 1];
const weightDiff = currentSecondFirst.weight - currentFirst.weight;
const prevWeightDiff = currentFirst.weight - prevLast.weight;
const diffIfFree = weightDiff - prevWeightDiff;
if (diffIfFree > maxDiff) {
maxDiff = diffIfFree;
}
}
if (maxDiff < dIfFree) {
for (let i = needFree; i > haveSpace; i--) {
const movedComb = categories.at(i).shift();
categories.at(i - 1).push(movedComb);
change = true;
}
}
}
}
} while (change && maxIterations-- > 0);
return categories;
}
const getCatNameList = (count) => {
const catNameList = [];
if (count >= 10) catNameList.push("Paille");
if (count >= 9) catNameList.push("Mouche");
if (count >= 8) catNameList.push("Coq");
if (count >= 7) catNameList.push("Plume");
if (count >= 3) catNameList.push("Léger");
if (count >= 5) catNameList.push("Mi-moyen");
if (count >= 1) catNameList.push("Moyen");
if (count >= 6) catNameList.push("Mi-lourd");
if (count >= 2) catNameList.push("Lourd");
if (count >= 4) catNameList.push("Super-lourd");
return catNameList;
}
export function AutoNewCatModalContent() {
const {t} = useTranslation("cm");
const {combs} = useCombs();
const {sendRequest} = useWS();
const toastId = React.useRef(null);
const [gender, setGender] = useState({H: false, F: false, NA: false})
const [cat, setCat] = useState([])
const [preset, setPreset] = useState(undefined)
const [lice, setLice] = useState("1")
const [classement, setClassement] = useState(true)
const [fullClassement, setFullClassement] = useState(false)
const setCat_ = (e, index) => {
if (e.target.checked) {
if (!cat.includes(index)) {
setCat([...cat, index])
}
} else {
setCat(cat.filter(c => c !== index))
}
}
function applyFilter(dataIn, dataOut) {
dataIn.forEach(comb => {
if (comb == null)
return;
if ((gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
&& (cat.includes(Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)))
&& (preset === undefined || comb.categoriesInscrites.includes(preset.id))) {
dataOut.push(comb)
}
}
)
}
const dispoFiltered = [];
if (combs != null)
applyFilter(Object.values(combs), dispoFiltered);
const handleSubmit = (e) => {
e.preventDefault();
let catList
if (dispoFiltered.length > 10) {
catList = makeWeightCategories(dispoFiltered);
} else {
catList = [[...dispoFiltered]];
}
console.log(catList.map(c => c.map(c => ({id: c.id, weight: c.weight, fname: c.fname, lname: c.lname}))))
toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0});
new Promise(async (resolve) => {
for (let i = 0; i < catList.length; i++) {
const progress = (i + 1) / catList.length;
toast.update(toastId.current, {progress});
const g = []
if (gender.H) g.push('H');
if (gender.F) g.push('F');
const type = catList[i].length > 5 && classement ? 3 : 1;
const newCat = {
name: preset.name + " - " + cat.map(pos => getCatName(CatList[pos])).join(", ") +
(gender.H && gender.F ? "" : " - " + g.join("/")) + (catList.length === 1 ? "" : " - " + getCatNameList(catList.length)[i]),
liceName: lice,
type: type,
treeAreClassement: classement,
fullClassement: fullClassement,
preset: {id: preset.id}
}
console.log(newCat)
await sendRequest('createCategory', newCat).then(id => {
newCat["id"] = id;
const groups = makePoule(catList[i], []);
const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups);
const p = [];
p.push(sendRequest("recalculateMatch", {
categorie: newCat.id,
newMatch,
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: []
}).then(() => {
console.log("Finished creating matches for category", newCat.name);
}).catch(err => {
console.error("Error creating matches for category", newCat.name, err);
}))
if (type === 3) {
const trees = build_tree(4, 1)
console.log("Creating trees for new category:", trees);
p.push(sendRequest('updateTrees', {
categoryId: id,
trees: trees
}).then(() => {
console.log("Finished creating trees for category", newCat.name);
}).catch(err => {
console.error("Error creating trees for category", newCat.name, err);
}))
}
return Promise.allSettled(p)
}).catch(err => {
console.error("Error creating category", newCat.name, err);
})
console.log("Finished category", i + 1, "/", catList.length);
}
resolve();
}).finally(() => {
toast.done(toastId.current);
})
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="autoNewCatModalLabel">{t('depuisUneCatégoriePrédéfinie')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="d-flex flex-wrap justify-content-around">
<ListPresetSelect value={preset} onChange={setPreset} returnId={false}/>
<div>
<label className="form-label">{t('genre')}</label>
<div className="d-flex align-items-center">
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
onChange={e => setGender((prev) => {
return {...prev, H: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
</div>
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
onChange={e => setGender((prev) => {
return {...prev, F: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
onChange={e => setGender((prev) => {
return {...prev, NA: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
</div>
</div>
</div>
</div>
{preset !== undefined && <>
<div className="d-flex flex-wrap justify-content-around mb-1">
<div className="d-flex flex-wrap mb-3">
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
{t('catégorie')} :
</label>
{preset.categories.map(c => [c.categorie, CatList.indexOf(c.categorie)]).sort((a, b) => a[1] - b[1])
.map(([cat_, index]) => {
return <div key={index} className="input-group"
style={{display: "contents"}}>
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox"
id={"categoriesInput" + index} checked={cat.includes(index)} aria-label={getCatName(cat_)}
onChange={e => setCat_(e, index)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{getCatName(cat_)}</label>
</div>
</div>
})}
</div>
</div>
</>}
<div className="mb-3">
<label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault" checked={classement}
onChange={e => setClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault">
{t('créerLaPhaseFinaleSilYADesPoules')}
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!classement}
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault2">
{t('lesCombattantsEnDehors2')}
</label>
</div>
<span>{dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')}</span><br/>
<span>{Math.ceil(dispoFiltered.length / 10)} {t('catégoriesVontêtreCréées')}</span><br/>
{dispoFiltered.length > 10 && dispoFiltered.some(c => !c.weight) &&
<span style={{color: "red"}}>{t('certainsCombattantsNontPasDePoidsRenseigné')}</span>}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" onClick={handleSubmit}
disabled={dispoFiltered.length <= 0} data-bs-dismiss="modal">{t('ajouter')}</button>
</div>
</>
}