dev #104

Merged
Thibaut merged 3 commits from dev into master 2026-01-18 15:19:13 +00:00
14 changed files with 325 additions and 54 deletions
Showing only changes of commit e8d5d0fa0c - Show all commits

View File

@ -10,6 +10,9 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter @Getter
@Setter @Setter
@AllArgsConstructor @AllArgsConstructor
@ -40,6 +43,22 @@ public class CompetitionGuestModel implements CombModel {
Integer weight = null; Integer weight = null;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable(
name = "groupe_membre",
joinColumns = @JoinColumn(name = "groupe_id"),
inverseJoinColumns = @JoinColumn(name = "membre_id")
)
List<MembreModel> comb = new ArrayList<>();
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable(
name = "groupe_guest",
joinColumns = @JoinColumn(name = "groupe_id"),
inverseJoinColumns = @JoinColumn(name = "guest_id")
)
List<CompetitionGuestModel> guest = new ArrayList<>();
public CompetitionGuestModel(String s) { public CompetitionGuestModel(String s) {
this.fname = s.substring(0, s.indexOf(" ")); this.fname = s.substring(0, s.indexOf(" "));
this.lname = s.substring(s.indexOf(" ") + 1); this.lname = s.substring(s.indexOf(" ") + 1);

View File

@ -9,6 +9,10 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@RegisterForReflection @RegisterForReflection
@ -23,6 +27,7 @@ public class CombEntity {
String country; String country;
int overCategory; int overCategory;
Integer weight; Integer weight;
List<CombEntity> teamMembers;
public static CombEntity fromModel(MembreModel model) { public static CombEntity fromModel(MembreModel model) {
if (model == null) if (model == null)
@ -31,7 +36,7 @@ public class CombEntity {
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(), return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
model.getClub() == null ? null : model.getClub().getClubId(), model.getClub() == null ? null : model.getClub().getClubId(),
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(), model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
0, null); 0, null, new ArrayList<>());
} }
@ -40,7 +45,9 @@ public class CombEntity {
return null; return null;
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null, return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null,
model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight()); model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight(),
Stream.concat(model.getComb().stream().map(CombEntity::fromModel),
model.getGuest().stream().map(CombEntity::fromModel)).toList());
} }
public static CombEntity fromModel(RegisterModel registerModel) { public static CombEntity fromModel(RegisterModel registerModel) {
@ -51,6 +58,6 @@ public class CombEntity {
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(), return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(), registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(),
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(), registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight()); model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight(), new ArrayList<>());
} }
} }

View File

@ -325,6 +325,9 @@ public class CompetitionService {
})) }))
.chain(model -> { .chain(model -> {
model.setFname(data.getFname()); model.setFname(data.getFname());
if (data.getLname().equals("__team"))
model.setLname("_team");
else
model.setLname(data.getLname()); model.setLname(data.getLname());
model.setGenre(data.getGenre()); model.setGenre(data.getGenre());
model.setClub(data.getClub()); model.setClub(data.getClub());
@ -467,7 +470,8 @@ public class CompetitionService {
.call(cm -> membreService.getById(combId) .call(cm -> membreService.getById(combId)
.invoke(Unchecked.consumer(model -> { .invoke(Unchecked.consumer(model -> {
if (model == null) if (model == null)
throw new DNotFoundException(String.format(trad.t("le.membre.n.existe.pas"), combId)); throw new DNotFoundException(
String.format(trad.t("le.membre.n.existe.pas"), combId));
if (!securityCtx.isInClubGroup(model.getClub().getId())) if (!securityCtx.isInClubGroup(model.getClub().getId()))
throw new DForbiddenException(); throw new DForbiddenException();
}))) })))

View File

@ -48,6 +48,9 @@ public class CompetitionWS {
@Inject @Inject
RCardboard rCardboard; RCardboard rCardboard;
@Inject
RTeam rTeam;
@Inject @Inject
SecurityCtx securityCtx; SecurityCtx securityCtx;
@ -91,6 +94,7 @@ public class CompetitionWS {
getWSReceiverMethods(RCategorie.class, rCategorie); getWSReceiverMethods(RCategorie.class, rCategorie);
getWSReceiverMethods(RRegister.class, rRegister); getWSReceiverMethods(RRegister.class, rRegister);
getWSReceiverMethods(RCardboard.class, rCardboard); getWSReceiverMethods(RCardboard.class, rCardboard);
getWSReceiverMethods(RTeam.class, rTeam);
executor = notifyExecutor; executor = notifyExecutor;
} }

View File

@ -0,0 +1,137 @@
package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.RegisterModel;
import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
import fr.titionfire.ffsaf.domain.entity.CombEntity;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.Pair;
import fr.titionfire.ffsaf.ws.PermLevel;
import fr.titionfire.ffsaf.ws.send.SSRegister;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
@RegisterForReflection
public class RTeam {
@Inject
TradService trad;
@Inject
CompetitionRepository competitionRepository;
@Inject
RegisterRepository registerRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@WSReceiver(code = "setTeam", permission = PermLevel.ADMIN)
public Uni<CombEntity> setTeam(WebSocketConnection connection, TeamData data) {
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
.chain(cm -> registerRepository.list("membre.id IN ?1 AND competition = ?2",
data.members.stream().filter(id -> id >= 0).toList(), cm)
.chain(l -> competitionGuestRepository.list("id IN ?1",
data.members.stream().filter(id -> id < 0).map(i -> i * -1).toList())
.map(l2 -> new Pair<>(l, l2)))
.chain(pair ->
competitionGuestRepository.find("fname = ?1 AND lname = ?2 AND competition = ?3",
data.name, "__team", cm).firstResult()
.chain(team -> {
if (pair.getKey().isEmpty() && pair.getValue().isEmpty()) {
if (team != null) {
CompetitionGuestModel finalTeam1 = team;
SSRegister.sendRegisterRemove(connection, finalTeam1.getId() * -1);
return Panache.withTransaction(
() -> competitionGuestRepository.delete(finalTeam1))
.replaceWith((CombEntity) null);
} else
return Uni.createFrom().item((CombEntity) null);
}
if (team == null) {
// Create new team
team = new CompetitionGuestModel();
team.setFname(data.name);
team.setLname("__team");
team.setCompetition(cm);
team.setClub("Team");
team.setGenre(Genre.NA);
} else {
team.getComb().clear();
team.getGuest().clear();
}
team.setCategorie(Stream.concat(
pair.getKey().stream().map(RegisterModel::getCategorie2),
pair.getValue().stream().map(CompetitionGuestModel::getCategorie))
.map(Enum::ordinal)
.max(Integer::compareTo)
.map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1));
List<Integer> s = Stream.concat(
pair.getKey().stream().map(RegisterModel::getWeight),
pair.getValue().stream().map(CompetitionGuestModel::getWeight))
.filter(Objects::nonNull).toList();
if (s.isEmpty()) {
team.setWeight(null);
} else if (s.size() == 1) {
team.setWeight(s.get(0));
} else {
team.setWeight((int) s.stream().mapToInt(Integer::intValue)
.average()
.orElse(0));
}
team.setCountry(Stream.concat(
pair.getKey().stream().map(m -> m.getMembre().getCountry()),
pair.getValue().stream().map(CompetitionGuestModel::getCountry))
.filter(Objects::nonNull)
.map(String::toUpperCase)
.collect(Collectors.groupingBy(
e -> e, // Classer par élément
Collectors.counting() // Compter les occurrences
))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("FR"));
team.getComb().addAll(
pair.getKey().stream().map(RegisterModel::getMembre).toList());
team.getGuest().addAll(pair.getValue());
CompetitionGuestModel finalTeam = team;
return Panache.withTransaction(
() -> competitionGuestRepository.persistAndFlush(finalTeam))
.map(CombEntity::fromModel);
}))
)
.invoke(combEntity -> {
if (combEntity != null)
SSRegister.sendRegister(connection, combEntity);
});
}
@RegisterForReflection
public record TeamData(List<Long> members, String name) {
}
}

View File

@ -0,0 +1,16 @@
package fr.titionfire.ffsaf.ws.send;
import fr.titionfire.ffsaf.domain.entity.CombEntity;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import io.quarkus.websockets.next.WebSocketConnection;
public class SSRegister {
public static void sendRegister(WebSocketConnection connection, CombEntity combEntity) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendRegister", combEntity);
}
public static void sendRegisterRemove(WebSocketConnection connection, Long combId) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendRegisterRemove", combId);
}
}

View File

@ -1,11 +1,13 @@
{ {
"--SélectionnerUnCombattant--": "-- Select a fighter --", "--SélectionnerUnCombattant--": "-- Select a fighter --",
"--Tous--": "-- All --", "--Tous--": "-- All --",
"PourLéquipe": "for the team",
"actuel": "Current", "actuel": "Current",
"administration": "Administration", "administration": "Administration",
"adresseDuServeur": "Server address", "adresseDuServeur": "Server address",
"ajouter": "Add", "ajouter": "Add",
"ajouterDesCombattants": "Add fighters", "ajouterDesCombattants": "Add fighters",
"ajouterUneTeam": "Add team",
"attention": "Warning", "attention": "Warning",
"aucuneConfigurationObs": "No OBS configuration found, please import one", "aucuneConfigurationObs": "No OBS configuration found, please import one",
"bleu": "Blue", "bleu": "Blue",
@ -69,6 +71,7 @@
"neRienConserver": "Keep nothing", "neRienConserver": "Keep nothing",
"no": "No.", "no": "No.",
"nom": "Name", "nom": "Name",
"nomDeLéquipe": "team name",
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>", "nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
"nouvelle...": "New...", "nouvelle...": "New...",
"obs.préfixDesSources": "Source prefix", "obs.préfixDesSources": "Source prefix",
@ -122,8 +125,12 @@
"toast.updateTrees.init.success": "Trees created!", "toast.updateTrees.init.success": "Trees created!",
"toast.updateTrees.pending": "Updating tournament trees...", "toast.updateTrees.pending": "Updating tournament trees...",
"toast.updateTrees.success": "Trees updated!", "toast.updateTrees.success": "Trees updated!",
"toast.team.update.error": "Error while updating team",
"toast.team.update.pending": "Updating team...",
"toast.team.update.success": "Team updated!",
"tournoi": "Tournament", "tournoi": "Tournament",
"tournois": "Tournaments", "tournois": "Tournaments",
"team": "Team",
"tousLesMatchs": "All matches", "tousLesMatchs": "All matches",
"toutConserver": "Keep all", "toutConserver": "Keep all",
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",

View File

@ -1,11 +1,13 @@
{ {
"--SélectionnerUnCombattant--": "-- Sélectionner un combattant --", "--SélectionnerUnCombattant--": "-- Sélectionner un combattant --",
"--Tous--": "-- Tous --", "--Tous--": "-- Tous --",
"PourLéquipe": " pour l'équipe",
"actuel": "Actuel", "actuel": "Actuel",
"administration": "Administration", "administration": "Administration",
"adresseDuServeur": "Adresse du serveur", "adresseDuServeur": "Adresse du serveur",
"ajouter": "Ajouter", "ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants", "ajouterDesCombattants": "Ajouter des combattants",
"ajouterUneTeam": "Ajouter une équipe",
"attention": "Attention", "attention": "Attention",
"aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une", "aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une",
"bleu": "Bleu", "bleu": "Bleu",
@ -69,6 +71,7 @@
"neRienConserver": "Ne rien conserver", "neRienConserver": "Ne rien conserver",
"no": "N°", "no": "N°",
"nom": "Nom", "nom": "Nom",
"nomDeLéquipe": "Nom de l'équipe",
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>", "nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
"nouvelle...": "Nouvelle...", "nouvelle...": "Nouvelle...",
"obs.préfixDesSources": "Préfix des sources", "obs.préfixDesSources": "Préfix des sources",
@ -122,8 +125,12 @@
"toast.updateTrees.init.success": "Arbres créés !", "toast.updateTrees.init.success": "Arbres créés !",
"toast.updateTrees.pending": "Mise à jour des arbres du tournoi...", "toast.updateTrees.pending": "Mise à jour des arbres du tournoi...",
"toast.updateTrees.success": "Arbres mis à jour !", "toast.updateTrees.success": "Arbres mis à jour !",
"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 !",
"tournoi": "Tournoi", "tournoi": "Tournoi",
"tournois": "Tournois", "tournois": "Tournois",
"team": "Équipe",
"tousLesMatchs": "Tous les matchs", "tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver", "toutConserver": "Tout conserver",
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",

View File

@ -23,6 +23,7 @@ function reducer(state, action) {
lname: action.payload.data.lname, lname: action.payload.data.lname,
genre: action.payload.data.genre, genre: action.payload.data.genre,
country: action.payload.data.country, country: action.payload.data.country,
teamMembers: action.payload.data.teamMembers,
}) })
if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) { if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) {
//console.debug("Updating comb", comb); //console.debug("Updating comb", comb);
@ -41,6 +42,7 @@ function reducer(state, action) {
lname: e.lname, lname: e.lname,
genre: e.genre, genre: e.genre,
country: e.country, country: e.country,
teamMembers: e.teamMembers,
} }
}); });
@ -71,7 +73,7 @@ function WSListener({dispatch}) {
useEffect(() => { useEffect(() => {
const sendRegister = ({data}) => { const sendRegister = ({data}) => {
dispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); dispatch({type: 'SET_COMB', payload: {source: "register", data: data}});
} }
dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}}) dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}})
@ -110,6 +112,8 @@ export function CombName({combId}) {
const {getComb} = useCombs(); const {getComb} = useCombs();
const comb = getComb(combId, null); const comb = getComb(combId, null);
if (comb) { if (comb) {
if (comb.lname === "__team")
return <>{comb.fname}</>
return <>{comb.fname} {comb.lname}</> return <>{comb.fname} {comb.lname}</>
} else { } else {
return <>[Comb #{combId}]</> return <>[Comb #{combId}]</>

View File

@ -500,15 +500,15 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
newTrees.push(trees2.at(i)); newTrees.push(trees2.at(i));
} }
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees") toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees", "cm")
).then(__ => { ).then(__ => {
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory")) toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
}) })
} }
}) })
confirmRef.current.click(); confirmRef.current.click();
} else { } else {
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory")) toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
} }
} }
@ -535,13 +535,13 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
name: name.trim(), name: name.trim(),
liceName: lice.trim(), liceName: lice.trim(),
type: nType type: nType
}), getToastMessage("toast.createCategory") }), getToastMessage("toast.createCategory", "cm")
).then(id => { ).then(id => {
if (tournoi) { if (tournoi) {
const trees = build_tree(size, loserMatch) const trees = build_tree(size, loserMatch)
console.log("Creating trees for new category:", trees); console.log("Creating trees for new category:", trees);
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init") toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init", "cm")
).finally(() => setCatId(id)) ).finally(() => setCatId(id))
} else { } else {
setCatId(id); setCatId(id);
@ -640,7 +640,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
title: t('confirm4.title'), title: t('confirm4.title'),
message: t('confirm4.msg', {name: state.name}), message: t('confirm4.msg', {name: state.name}),
confirm: () => { confirm: () => {
toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory") toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory", "cm")
).then(() => setCatId(null)); ).then(() => setCatId(null));
} }
}) })

View File

@ -403,7 +403,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
menuActions.current.saveScore = (scoreRed, scoreBlue) => { menuActions.current.saveScore = (scoreRed, scoreBlue) => {
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue}; const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore")); toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore", "cm"));
} }
return () => menuActions.current.saveScore = undefined; return () => menuActions.current.saveScore = undefined;
}, [matchId]) }, [matchId])

View File

@ -170,6 +170,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
const combDispatch = useCombsDispatch() const combDispatch = useCombsDispatch()
const {dispatch} = useWS() const {dispatch} = useWS()
const [modalId, setModalId] = useState(null) const [modalId, setModalId] = useState(null)
const [modalMode, setModalMode] = useState(false)
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
@ -220,13 +221,17 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
return <> return <>
<GroupsList groups={groups} setModalId={setModalId}/> <GroupsList groups={groups} setModalId={setModalId}/>
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal" <button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
disabled={data === null}>{t('ajouterDesCombattants')} disabled={data === null} onClick={() => setModalMode(false)}>{t('ajouterDesCombattants')}
</button>
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
disabled={data === null} onClick={() => setModalMode(true)}>{t('ajouterUneTeam')}
</button> </button>
<div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true"> <div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true">
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down"> <div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
<div className="modal-content"> <div className="modal-content">
<SelectCombModalContent data={data} setGroups={setGroups}/> <SelectCombModalContent data={data} setGroups={setGroups} teamMode={modalMode}/>
</div> </div>
</div> </div>
</div> </div>
@ -247,10 +252,13 @@ function GroupsList({groups, setModalId}) {
const groups2 = groups.map(g => { const groups2 = groups.map(g => {
const comb = getComb(g.id); const comb = getComb(g.id);
return {...g, name: comb ? comb.fname + " " + comb.lname : ""}; return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
}).sort((a, b) => { }).sort((a, b) => {
if (a.poule !== b.poule) if (a.poule !== b.poule) {
if (a.poule === '-') return 1;
if (b.poule === '-') return -1;
return a.poule.localeCompare(b.poule); return a.poule.localeCompare(b.poule);
}
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}).reduce((acc, curr) => { }).reduce((acc, curr) => {
const poule = curr.poule; const poule = curr.poule;
@ -264,12 +272,20 @@ function GroupsList({groups, setModalId}) {
return <> return <>
{Object.keys(groups2).map((poule) => ( {Object.keys(groups2).map((poule) => (
<div key={poule} className="mb-3"> <div key={poule} className="mb-3">
<h5>{poule !== '-' ? (t('poule') +" : " + poule) : t('sansPoule')}</h5> <h5>{poule !== '-' ? (t('poule') + " : " + poule) : t('sansPoule')}</h5>
<ol className="list-group list-group-numbered"> <ol className="list-group list-group-numbered">
{groups2[poule].map((comb) => ( {groups2[poule].map((comb) => (
<li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start" <li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start"
data-bs-toggle="modal" data-bs-target="#groupeModal" onClick={_ => setModalId(comb.id)}> data-bs-toggle="modal" data-bs-target="#groupeModal" onClick={_ => setModalId(comb.id)}>
<div className="ms-2 me-auto"><CombName combId={comb.id}/></div> <div className="ms-2 me-auto">
<CombName combId={comb.id}/>
{comb.teamMembers.length > 0 && <>
{comb.teamMembers.map((m) => (<small key={m.id}>
<br/>
<CombName combId={m.id}/>
</small>))}
</>}
</div>
<span className="badge text-bg-primary rounded-pill">{comb.poule}</span> <span className="badge text-bg-primary rounded-pill">{comb.poule}</span>
</li>) </li>)
)} )}
@ -374,7 +390,7 @@ function ListMatch({cat, matches, groups, reducer}) {
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: matchesToRemove.map(m => m.id) matchesToRemove: matchesToRemove.map(m => m.id)
}), getToastMessage("toast.matchs.create")) }), getToastMessage("toast.matchs.create", "ns"))
.finally(() => { .finally(() => {
console.log("Finished creating matches"); console.log("Finished creating matches");
}) })

View File

@ -1,9 +1,10 @@
import {useCountries} from "../../../hooks/useCountries.jsx"; import {useCountries} from "../../../hooks/useCountries.jsx";
import {useEffect, useReducer, useState} from "react"; import {useEffect, useReducer, useState} from "react";
import {CatList, getCatName} from "../../../utils/Tools.js"; import {CatList, getCatName, getToastMessage} from "../../../utils/Tools.js";
import {CombName} from "../../../hooks/useComb.jsx"; import {CombName} from "../../../hooks/useComb.jsx";
import {useWS} from "../../../hooks/useWS.jsx"; import {useWS} from "../../../hooks/useWS.jsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {toast} from "react-toastify";
function SelectReducer(state, action) { function SelectReducer(state, action) {
switch (action.type) { switch (action.type) {
@ -53,14 +54,14 @@ function SelectReducer(state, action) {
} }
} }
export function SelectCombModalContent({data, setGroups}) { export function SelectCombModalContent({data, setGroups, teamMode = false}) {
const country = useCountries('fr') const country = useCountries('fr')
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const {dispatch} = useWS() const {sendRequest, dispatch} = useWS()
const [dispo, dispoReducer] = useReducer(SelectReducer, {}) const [dispo, dispoReducer] = useReducer(SelectReducer, {})
const [select, selectReducer] = useReducer(SelectReducer, {}) const [select, selectReducer] = useReducer(SelectReducer, {})
const [targetGroupe, setTargetGroupe] = useState("A") const [targetGroupe, setTargetGroupe] = useState("1")
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [country_, setCountry_] = useState("") const [country_, setCountry_] = useState("")
const [club, setClub] = useState("") const [club, setClub] = useState("")
@ -68,12 +69,27 @@ export function SelectCombModalContent({data, setGroups}) {
const [cat, setCat] = useState(-1) const [cat, setCat] = useState(-1)
const [weightMin, setWeightMin] = useState(0) const [weightMin, setWeightMin] = useState(0)
const [weightMax, setWeightMax] = useState(0) const [weightMax, setWeightMax] = useState(0)
const [team, setTeam] = useState(false)
const [teamName, setTeamName] = useState("");
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (teamMode) {
toast.promise(
sendRequest('setTeam', {
name: teamName,
members: [...Object.keys(select).map(id => Number(id))]
}), getToastMessage("toast.team.update", "cm"))
.then(res => {
if (res && res.id) {
setGroups(prev => [...prev.filter(d => d.id !== Number(res.id)), {id: Number(res.id), poule: targetGroupe}])
}
})
} else {
setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => { setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => {
return {id: Number(id), poule: targetGroupe} return {id: Number(id), poule: targetGroupe}
})]) })])
}
dispoReducer({type: 'REMOVE_ALL'}) dispoReducer({type: 'REMOVE_ALL'})
selectReducer({type: 'REMOVE_ALL'}) selectReducer({type: 'REMOVE_ALL'})
@ -101,6 +117,18 @@ export function SelectCombModalContent({data, setGroups}) {
dispoReducer({type: 'ADD_ALL', payload: data.map(d => d.id).filter(id => !selectedIds.includes(id))}) dispoReducer({type: 'ADD_ALL', payload: data.map(d => d.id).filter(id => !selectedIds.includes(id))})
}, [data]) }, [data])
useEffect(() => {
if (data == null)
return
const teamIds = data.filter(d => d.teamMembers != null && d.teamMembers.length > 0).map(t => String(t.id));
if (teamMode) {
dispoReducer({type: 'REMOVE_IN', payload: teamIds});
selectReducer({type: 'REMOVE_IN', payload: teamIds});
} else {
dispoReducer({type: 'ADD_ALL', payload: teamIds});
}
}, [teamMode])
function applyFilter(dataIn, dataOut) { function applyFilter(dataIn, dataOut) {
Object.keys(dataIn).forEach((id) => { Object.keys(dataIn).forEach((id) => {
const comb = data.find(d => d.id === Number(id)); const comb = data.find(d => d.id === Number(id));
@ -113,7 +141,9 @@ export function SelectCombModalContent({data, setGroups}) {
&& (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA') && (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
&& (cat === -1 || cat === Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)) && (cat === -1 || cat === Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory))
&& (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin) && (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin)
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)) { && (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))) {
dataOut[id] = dataIn[id]; dataOut[id] = dataIn[id];
} }
} }
@ -155,7 +185,7 @@ export function SelectCombModalContent({data, setGroups}) {
return <> return <>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">{t('select.sélectionnerDesCombatants')}</h1> <h1 className="modal-title fs-5" id="CategorieModalLabel">{t('select.sélectionnerDesCombatants')}{teamMode && (t('PourLéquipe'))}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
@ -225,6 +255,16 @@ export function SelectCombModalContent({data, setGroups}) {
})} })}
</select> </select>
</div> </div>
{!teamMode && <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> <div>
<label htmlFor="input5" className="form-label">{t('poids')}</label> <label htmlFor="input5" className="form-label">{t('poids')}</label>
<div className="row-cols-sm-auto d-flex align-items-center"> <div className="row-cols-sm-auto d-flex align-items-center">
@ -277,13 +317,19 @@ export function SelectCombModalContent({data, setGroups}) {
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<div className="vr"></div> <div className="vr"></div>
<label htmlFor="input6" className="form-label">{t('poule')}</label> {teamMode && <>
<input type="text" className="form-control" id="input6" style={{width: "3em"}} maxLength={1} value={targetGroupe} <label htmlFor="input6" className="form-label">{t('nomDeLéquipe')}</label>
<input type="text" className="form-control" id="input6" style={{width: "10em"}} value={teamName}
onChange={(e) => setTeamName(e.target.value)}/>
</>}
<label htmlFor="input7" className="form-label">{t('poule')}</label>
<input type="text" className="form-control" id="input7" style={{width: "3em"}} maxLength={1} value={targetGroupe}
onChange={(e) => { onChange={(e) => {
if (/^[a-zA-Z0-9]$/.test(e.target.value)) if (/^[a-zA-Z0-9]?/.test(e.target.value))
setTargetGroupe(e.target.value) setTargetGroupe(e.target.value)
}}/> }}/>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>{t('ajouter')}</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}
disabled={targetGroupe.length === 0 || (teamMode && teamName.length === 0)}>{t('ajouter')}</button>
</div> </div>
</> </>
} }

View File

@ -19,6 +19,10 @@ export function isClubAdmin(userinfo) {
export const errFormater = (data, msg) => { export const errFormater = (data, msg) => {
if (!data)
return msg
if (!data.response)
return `${msg} (😕 ${data.message})`
if (typeof data.response.data === 'string' || data.response.data instanceof String) if (typeof data.response.data === 'string' || data.response.data instanceof String)
return `${msg} (${data.response.statusText}: ${data.response.data}) 😕` return `${msg} (${data.response.statusText}: ${data.response.data}) 😕`
return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕` return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕`
@ -107,13 +111,13 @@ export function getCatName(cat) {
} }
} }
export function getToastMessage(msgKey) { export function getToastMessage(msgKey, ns = 'common') {
return { return {
pending: i18n.t(msgKey + '.pending'), pending: i18n.t(msgKey + '.pending', {ns}),
success: i18n.t(msgKey + '.success'), success: i18n.t(msgKey + '.success', {ns}),
error: { error: {
render({data}) { render({data}) {
return errFormater(data, i18n.t(msgKey + '.error')) return errFormater(data, i18n.t(msgKey + '.error', {ns}))
} }
} }
} }
@ -148,7 +152,7 @@ export function timePrint(time, negSign = false) {
if (time === null || time === undefined) if (time === null || time === undefined)
return "" return ""
const neg = time < 0 const neg = time < 0
if (neg){ if (neg) {
if (!negSign) if (!negSign)
return "00:00" return "00:00"
time = -time time = -time
@ -168,22 +172,22 @@ export function timePrint(time, negSign = false) {
} }
//create full hex //create full hex
function fullHex (hex) { function fullHex(hex) {
let r = hex.slice(1,2); let r = hex.slice(1, 2);
let g = hex.slice(2,3); let g = hex.slice(2, 3);
let b = hex.slice(3,4); let b = hex.slice(3, 4);
r = parseInt(r+r, 16); r = parseInt(r + r, 16);
g = parseInt(g+g, 16); g = parseInt(g + g, 16);
b = parseInt(b+b, 16); b = parseInt(b + b, 16);
// return {r, g, b} // return {r, g, b}
return { r, g, b }; return {r, g, b};
} }
//convert hex to rgb //convert hex to rgb
export function hex2rgb (hex) { export function hex2rgb(hex) {
if(hex.length === 4){ if (hex.length === 4) {
return fullHex(hex); return fullHex(hex);
} }
@ -192,5 +196,5 @@ export function hex2rgb (hex) {
const b = parseInt(hex.slice(5, 7), 16); const b = parseInt(hex.slice(5, 7), 16);
// return {r, g, b} // return {r, g, b}
return { r, g, b }; return {r, g, b};
} }