feat: auto categories full competition

This commit is contained in:
Thibaut Valentin 2026-02-13 21:33:19 +01:00
parent d43cdc1a4e
commit 2fd09af0ea
5 changed files with 329 additions and 81 deletions

View File

@ -29,6 +29,7 @@ import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@WithSession
@ -130,9 +131,39 @@ public class RCategorie {
.map(CategoryModel::getId);
}
@WSReceiver(code = "createOrReplaceCategory", permission = PermLevel.ADMIN)
public Uni<Long> createOrReplaceCategory(WebSocketConnection connection, JustCategorie categorie) {
return matchRepository.list("category.compet.uuid = ?1 AND category.name = ?2", connection.pathParam("uuid"),
categorie.name)
.chain(existing -> {
if (existing.isEmpty())
return createCategory(connection, categorie);
Map<Long, List<MatchModel>> matchesByCategory = existing.stream()
.filter(m -> m.getCategory() != null)
.collect(Collectors.groupingBy(m -> m.getCategory().getId()));
for (Map.Entry<Long, List<MatchModel>> entry : matchesByCategory.entrySet()) {
Long categoryId = entry.getKey();
List<MatchModel> matches = entry.getValue();
if (matches.stream().noneMatch(m -> !m.getScores().isEmpty() || m.isEnd()))
return Panache.withTransaction(() -> updateCategory(connection, categorie, categoryId)
.call(__ -> treeRepository.delete("category = ?1", categoryId))
.call(__ -> matchRepository.delete("category.id = ?1", categoryId)))
.replaceWith(categoryId);
}
return createCategory(connection, categorie);
});
}
@WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN)
public Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie) {
return getById(categorie.id, connection)
return updateCategory(connection, categorie, categorie.id);
}
private Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie, Long id) {
return getById(id, connection)
.call(cat -> {
if (categorie.preset() == null) {
cat.setPreset(null);

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, {useEffect, useId, useState} from "react";
import {Trans, useTranslation} from "react-i18next";
import {useCountries} from "../../hooks/useCountries.jsx";
import {ListPresetSelect} from "./ListPresetSelect.jsx";
@ -7,7 +7,8 @@ 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";
import {useRequestWS, useWS} from "../../hooks/useWS.jsx";
import {AxiosError} from "../AxiosError.jsx";
export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) {
const country = useCountries('fr')
@ -369,67 +370,112 @@ const getCatNameList = (count) => {
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 >= 2) catNameList.push("Léger");
if (count >= 5) catNameList.push("Mi-moyen");
if (count >= 1) catNameList.push("Moyen");
if (count >= 3) catNameList.push("Moyen");
if (count >= 6) catNameList.push("Mi-lourd");
if (count >= 2) catNameList.push("Lourd");
if (count >= 1) 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);
function makeCategory(combs) {
const out = Array.from(CatList, (v, i) => ({
h: combs.filter(c => c.categorie === v && c.genre !== "F"),
f: combs.filter(c => c.categorie === v && c.genre === "F"),
m: [],
canMakeGenreFusion: i <= CatList.indexOf("BENJAMIN"),
c: v,
c_index: i,
min_c_index: i,
done: false
}))
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)
for (let i = 0; i < out.length; i++)
out[i].done = out[i].h.length === 0 && out[i].f.length === 0;
const setCat_ = (e, index) => {
if (e.target.checked) {
if (!cat.includes(index)) {
setCat([...cat, index])
for (let i = 0; i < out.length - 1; i++) {
const p = i === 0 ? undefined : out[i - 1];
const c = out[i];
const n = out[i + 1];
if (c.done)
continue;
if (c.canMakeGenreFusion) {
if (c.f.length < 6 || c.h.length < 5) {
if (c.f.length + c.h.length >= 3) {
c.m = c.h.concat(c.f);
c.h = [];
c.f = [];
c.done = true;
} else {
n.h = n.h.concat(c.h);
n.f = n.f.concat(c.f);
n.min_c_index = c.min_c_index
c.h = [];
c.f = [];
c.done = true;
}
} else {
setCat(cat.filter(c => c !== index))
c.done = true;
}
}
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]];
if (c.h.length < 3 && c.h.length > 0) {
if (p) {
if (p.h.length > 0 && p.h.length + c.h.length <= c.h.length + n.h.length && p.min_c_index - p.c_index < 1) {
p.h = p.h.concat(c.h);
c.h = [];
} else {
n.h = n.h.concat(c.h);
c.h = [];
}
} else {
n.h = n.h.concat(c.h);
c.h = [];
}
}
if (c.f.length < 3 && c.f.length > 0) {
if (p) {
if (p.f.length > 0 && p.f.length + c.f.length <= c.f.length + n.f.length && p.min_c_index - p.c_index < 1) {
p.f = p.f.concat(c.f);
c.f = [];
} else {
n.f = n.f.concat(c.f);
c.f = [];
}
} else {
n.f = n.f.concat(c.f);
c.f = [];
}
}
c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0);
}
}
console.log(catList.map(c => c.map(c => ({id: c.id, weight: c.weight, fname: c.fname, lname: c.lname}))))
// Down fusion if not done
for (let i = out.length - 1; i > 0; i--) {
const p = out[i - 1];
const c = out[i];
if (c.done)
continue;
if (c.h.length > 0 && c.h.length < 3) {
p.h = p.h.concat(c.h);
c.h = [];
}
if (c.f.length > 0 && c.f.length < 3) {
p.f = p.f.concat(c.f);
c.f = [];
}
c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0);
p.done = (p.h.length >= 3 || p.h.length === 0) && (p.f.length >= 3 || p.f.length === 0);
}
return out.map(c => [c.h, c.f, c.m]).flat().filter(l => l.length > 0);
}
function sendCatList(toastId, t, catList, sendRequest) {
toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0});
new Promise(async (resolve) => {
@ -438,24 +484,30 @@ export function AutoNewCatModalContent() {
toast.update(toastId.current, {progress});
const g = []
if (gender.H) g.push('H');
if (gender.F) g.push('F');
if (catList[i].combs.some(c => c.genre === "H")) g.push('H');
if (catList[i].combs.some(c => c.genre === "F")) g.push('F');
const type = catList[i].length > 5 && classement ? 3 : 1;
const cat = []
catList[i].combs.forEach(c => {
if (!cat.includes(c.categorie))
cat.push(c.categorie);
})
const type = catList[i].combs.length > 5 && catList[i].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,
name: catList[i].preset.name + " - " + cat.map(c => getCatName(c)).join(", ") +
(g.length === 2 ? "" : " - " + g.join("/")) + (catList[i].size === 1 ? "" : " - " + getCatNameList(catList[i].size)[catList[i].index]),
liceName: catList[i].lice,
type: type,
treeAreClassement: classement,
fullClassement: fullClassement,
preset: {id: preset.id}
treeAreClassement: catList[i].classement,
fullClassement: catList[i].fullClassement,
preset: {id: catList[i].preset.id}
}
console.log(newCat)
await sendRequest('createCategory', newCat).then(id => {
await sendRequest('createOrReplaceCategory', newCat).then(id => {
newCat["id"] = id;
const groups = makePoule(catList[i], []);
const groups = makePoule(catList[i].combs, []);
const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups);
const p = [];
@ -497,6 +549,61 @@ export function AutoNewCatModalContent() {
})
}
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}))))
sendCatList(toastId, t, catList
.map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})), sendRequest);
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="autoNewCatModalLabel">{t('depuisUneCatégoriePrédéfinie')}</h1>
@ -589,3 +696,107 @@ export function AutoNewCatModalContent() {
</div>
</>
}
export function AutoNewCatSModalContent() {
const {t} = useTranslation("cm");
const {combs} = useCombs();
const {sendRequest} = useWS();
const toastId = React.useRef(null);
const {data, error} = useRequestWS("listPreset", {}, null);
const id = useId()
const [categories, setCategories] = useState([])
const [lice, setLice] = useState("1")
const [classement, setClassement] = useState(true)
const [fullClassement, setFullClassement] = useState(false)
const setCategories_ = (e, catId) => {
if (e.target.checked) {
if (!categories.includes(catId)) {
setCategories([...categories, catId])
}
} else {
setCategories(categories.filter(c => c !== catId))
}
}
const handleSubmit = (e) => {
e.preventDefault();
let catList2 = []
for (const catId of categories) {
const preset = data.find(p => p.id === catId);
const dispoFiltered = Object.values(combs).filter(comb => comb.categoriesInscrites?.includes(catId)).sort(() => Math.random() - 0.5)
.map(comb => ({...comb, categorie: CatList[Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)]}));
console.log("Creating category for preset", preset.name, "and", dispoFiltered.length, "combattants");
const catList = makeCategory(dispoFiltered);
console.log(catList)
for (const list of catList) {
if (list.length > 10) {
catList2.push(...makeWeightCategories(list)
.map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})));
} else {
catList2.push(({combs: [...list], classement, preset, lice, fullClassement, index: 1, size: 1}));
}
}
}
sendCatList(toastId, t, catList2, sendRequest);
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="autoNewCatSModalLabel">{t('créerToutesLesCatégories')}</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">
<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>
{error ? <AxiosError error={error}/> : <>
{data && data.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
{data && data.map((cat, index) =>
<div key={cat.id} className="input-group"
style={{display: "contents"}}>
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox"
id={id + "categoriesInput" + index} checked={categories.includes(cat.id)} aria-label={cat.name}
onChange={e => setCategories_(e, cat.id)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={id + "categoriesInput" + index}>{cat.name}</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>
</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}>{t('ajouter')}</button>
</div>
</>
}

View File

@ -20,7 +20,7 @@ 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} from "../../../components/cm/AutoCatModalContent.jsx";
import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
const vite_url = import.meta.env.VITE_URL;
@ -597,15 +597,6 @@ function CategoryHeader({
</div>
</div>
<div className="modal fade" id="exampleModalToggle2" aria-hidden="true" aria-labelledby="exampleModalToggleLabel2" tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<AutoNewCatModalContent/>
</div>
</div>
</div>
<div className="modal fade" id="autoNewCatModal" aria-hidden="true" aria-labelledby="autoNewCatModalLabel" tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
@ -614,6 +605,14 @@ function CategoryHeader({
</div>
</div>
<div className="modal fade" id="autoNewCatsModal" aria-hidden="true" aria-labelledby="autoNewCatsModalLabel" tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<AutoNewCatSModalContent/>
</div>
</div>
</div>
<button ref={confirmRef} data-bs-toggle="modal" data-bs-target="#confirm-dialog" style={{display: "none"}}>open</button>
<ConfirmDialog id="confirm-dialog" onConfirm={confirm.confirm ? confirm.confirm : () => {
}} onCancel={confirm.cancel ? confirm.cancel : () => {
@ -643,7 +642,8 @@ function CategoryHeader({
data-bs-toggle="modal">{t('depuisUneCatégoriePrédéfinie')}</button>
</div>
<div className="mb-2">
<button className="btn btn-primary" disabled={true}>{t('créerToutesLesCatégories')}</button>
<button className="btn btn-primary" data-bs-target="#autoNewCatsModal"
data-bs-toggle="modal">{t('créerToutesLesCatégories')}</button>
</div>
</div>
</div>

View File

@ -30,6 +30,7 @@ function CupImg() {
style={{width: "16px"}} src="/img/171891.png"
alt=""/>
}
function CupImg2() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png"
@ -85,6 +86,9 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
if (data.categorie !== cat.id)
continue;
setGroups(prev => {
if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id))
return [...prev, {id: data.c1?.id, poule: data.poule}];
@ -717,7 +721,8 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
<th style={{textAlign: "center", cursor: "auto"}} scope="row">{index + 1}</th>
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>}
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</td>}
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && ((m.win > 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && ((m.win > 0 &&
<CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}
onClick={e => handleCombClick(e, m.id, m.c1)}>
<small className="position-relative"><CombName combId={m.c1}/>
@ -726,7 +731,8 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
onClick={e => handleCombClick(e, m.id, m.c2)}>
<small className="position-relative"><CombName combId={m.c2}/>
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && ((m.win < 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && ((m.win < 0 &&
<CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "center", cursor: "auto"}}>{scoreToString2(m, cards_v)}</td>
<td style={{textAlign: "center", cursor: "pointer", color: "#1381ff"}} onClick={_ => handleEditMatch(m.id)}>
<FontAwesomeIcon icon={faPen}/></td>

View File

@ -156,7 +156,7 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)
&& (teamMode && (comb.teamMembers == null || comb.teamMembers.length === 0) || !teamMode
&& ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team))
&& (preset === -1 || comb.categoriesInscrites.includes(preset))) {
&& (preset === -1 || comb.categoriesInscrites?.includes(preset))) {
dataOut[id] = dataIn[id];
}
}