feat: card for team

This commit is contained in:
Thibaut Valentin 2026-01-29 22:35:54 +01:00
parent d749dea6f4
commit 9018ecef12
17 changed files with 679 additions and 64 deletions

View File

@ -6,6 +6,9 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.util.Date;
@Getter
@Setter
@ -27,6 +30,12 @@ public class CardModel {
Long competition;
CardType type;
String reason;
@CreationTimestamp
Date date;
@Column(nullable = false, columnDefinition = "boolean default false")
boolean teamCard = false;
public enum CardType {
BLUE,

View File

@ -0,0 +1,38 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.util.Date;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "card_team")
public class ClubCardModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
Long competition;
String teamUuid;
String teamName;
CardModel.CardType type;
String reason;
@CreationTimestamp
Date date;
List<Long> cardIds;
}

View File

@ -0,0 +1,10 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.ClubCardModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ClubCardRepository implements PanacheRepositoryBase<ClubCardModel, Long> {
}

View File

@ -1,11 +1,13 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.CardModel;
import fr.titionfire.ffsaf.data.model.ClubCardModel;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.CardRepository;
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.ws.recv.RCard;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.panache.common.Sort;
import io.smallrye.mutiny.Uni;
@ -13,9 +15,9 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@WithSession
@ApplicationScoped
@ -23,6 +25,18 @@ public class CardService {
@Inject
CardRepository cardRepository;
@Inject
ClubCardRepository clubCardRepository;
@Inject
RegisterRepository registerRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
MatchRepository matchRepository;
@Inject
TradService trad;
@ -45,15 +59,6 @@ public class CardService {
return ids;
}
private Collection<Long> extractCombIds(MatchEntity match) {
List<Long> ids = new ArrayList<>();
if (match.getC1() != null)
ids.add(match.getC1().getId());
if (match.getC2() != null)
ids.add(match.getC2().getId());
return ids;
}
public Uni<List<CardModel>> getForMatch(MatchModel match) {
return cardRepository.list(
"competition = ?1 AND (type IN ?2 OR (type = CardType.BLUE AND category = ?4)) AND comb IN ?3",
@ -88,4 +93,102 @@ public class CardService {
return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added")));
});
}
public Uni<List<CardModel>> addTeamCard(CompetitionModel competition, String teamUuid, String teamName,
CardModel.CardType type, String reason) {
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)",
Sort.descending("type"), competition.getId(), teamUuid, teamName)
.firstResult()
.map(card_ -> {
if (type == CardModel.CardType.BLACK) {
return card_ != null && card_.getType() == CardModel.CardType.RED;
}
return card_ == null || card_.getType().ordinal() < type.ordinal();
})
.chain(b -> {
if (!b)
return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added")));
if (teamUuid != null) {
return registerRepository.list("competition = ?1 AND club.clubId = ?2", competition, teamUuid)
.map(l -> l.stream().map(r -> r.getMembre().getId()).toList());
} else {
return competitionGuestRepository.list("competition = ?1 AND club = ?2", competition,
teamName)
.map(l -> l.stream().map(r -> r.getId() * -1).toList());
}
})
.chain(combIds -> cardRepository.list("competition = ?1 AND comb IN ?2", competition.getId(), combIds)
.map(cards -> {
List<CardModel> newCards = new ArrayList<>();
for (Long id : combIds) {
Optional<CardModel> optional = cards.stream()
.filter(c -> id.equals(c.getComb()) && c.getType() == type).findAny();
CardModel model = new CardModel();
model.setCompetition(competition.getId());
model.setComb(id);
model.setTeamCard(true);
if (optional.isEmpty()) {
model.setType(type);
} else {
model.setType(
CardModel.CardType.values()[Math.min(optional.get().getType().ordinal() + 1,
CardModel.CardType.BLACK.ordinal())]);
}
newCards.add(model);
}
return newCards;
})
)
.call(newCards -> Panache.withTransaction(() -> cardRepository.persist(newCards)))
.call(newCards -> {
ClubCardModel model = new ClubCardModel();
model.setCompetition(competition.getId());
model.setTeamUuid(teamUuid);
model.setTeamName(teamName);
model.setType(type);
model.setReason(reason);
model.setCardIds(newCards.stream().map(CardModel::getId).toList());
return Panache.withTransaction(() -> clubCardRepository.persist(model));
});
}
public Uni<List<CardModel>> recvReturnState(CompetitionModel competition, RCard.SendTeamCardReturnState state) {
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3) AND type = ?4",
competition.getId(), state.teamUuid(), state.teamName(), state.type())
.firstResult()
.chain(o -> cardRepository.list("id IN ?1", o.getCardIds()))
.call(cards -> matchRepository.list("category.compet = ?1 AND category.id IN ?2", competition,
state.selectedCategory())
.invoke(matches -> {
for (CardModel card : cards) {
for (MatchModel m : matches.stream()
.filter(m -> extractCombIds(m).contains(card.getComb())).toList()) {
if (state.state() == 1) {
card.setCategory(m.getCategory().getId());
} else if (state.state() == 2) {
card.setCategory(m.getCategory().getId());
if (Objects.equals(m.getId(), state.selectedMatch()))
card.setMatch(m.getId());
}
}
}
})
.chain(() -> Panache.withTransaction(() -> cardRepository.persist(cards))));
}
public Uni<List<Long>> rmTeamCard(CompetitionModel competition, String teamUuid, String teamName,
CardModel.CardType type) {
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3) AND type = ?4",
competition.getId(), teamUuid, teamName, type)
.firstResult()
.chain(card -> Uni.createFrom().item(card.getCardIds())
.call(() -> Panache.withTransaction(() -> cardRepository.delete("id IN ?1", card.getCardIds())))
.call(() -> Panache.withTransaction(() -> clubCardRepository.delete(card))));
}
}

View File

@ -3,8 +3,8 @@ package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CardModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.CardRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository;
import fr.titionfire.ffsaf.data.repository.ClubCardRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.domain.service.CardService;
import fr.titionfire.ffsaf.domain.service.TradService;
@ -21,6 +21,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Date;
import java.util.List;
@WithSession
@ -31,6 +32,9 @@ public class RCard {
@Inject
MatchRepository matchRepository;
@Inject
ClubCardRepository clubCardRepository;
@Inject
CardRepository cardRepository;
@ -38,10 +42,7 @@ public class RCard {
CardService cardService;
@Inject
CombRepository combRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
CompetitionRepository competitionRepository;
@Inject
TradService trad;
@ -63,6 +64,16 @@ public class RCard {
return getById(matchId, connection).chain(matchModel -> cardService.getForMatch(matchModel));
}
@WSReceiver(code = "getAllForTeamNoDetail", permission = PermLevel.VIEW)
public Uni<List<SendTeamCards>> getAllForTeamNoDetail(WebSocketConnection connection, Object o) {
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
.chain(c -> clubCardRepository.list("competition = ?1", c.getId()))
.map(cards -> cards.stream()
.map(card -> new SendTeamCards(card.getTeamUuid(), card.getTeamName(), List.of(),
card.getType(), card.getReason(), card.getDate()))
.toList());
}
@WSReceiver(code = "sendCardAdd", permission = PermLevel.TABLE)
public Uni<Void> sendCardAdd(WebSocketConnection connection, SendCardAdd card) {
return getById(card.matchId(), connection)
@ -74,11 +85,12 @@ public class RCard {
model.setCategory(matchModel.getCategory().getId());
model.setCompetition(matchModel.getCategory().getCompet().getId());
model.setType(card.type());
model.setReason(card.reason());
return Panache.withTransaction(() -> cardRepository.persist(model));
})
)
.invoke(cardModel -> SSCard.sendCard(connection, cardModel))
.invoke(cardModel -> SSCard.sendCards(connection, List.of(cardModel)))
.replaceWithVoid();
}
@ -91,14 +103,55 @@ public class RCard {
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException(trad.t("carton.non.trouver"));
SSCard.sendRmCard(connection, o.getId());
SSCard.sendRmCards(connection, List.of(o.getId()));
}))
.chain(cardModel -> Panache.withTransaction(() -> cardRepository.delete(cardModel)))
)
.replaceWithVoid();
}
@WSReceiver(code = "applyTeamCards", permission = PermLevel.TABLE)
public Uni<Void> applyTeamCards(WebSocketConnection connection, SendTeamCards teamCards) {
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
.chain(c -> cardService.addTeamCard(c, teamCards.teamUuid(), teamCards.teamName(), teamCards.type,
teamCards.reason()))
.invoke(cards -> SSCard.sendTeamCard(connection,
new SendTeamCards(teamCards.teamUuid(), teamCards.teamName(), cards, teamCards.type(),
teamCards.reason(), new Date())))
.replaceWithVoid();
}
@WSReceiver(code = "sendTeamCardReturnState", permission = PermLevel.TABLE)
public Uni<Void> sendTeamCardReturnState(WebSocketConnection connection, SendTeamCardReturnState state) {
if (state.state <= 0 || state.state > 2)
return Uni.createFrom().voidItem();
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
.chain(c -> cardService.recvReturnState(c, state))
.invoke(cards -> SSCard.sendCards(connection, cards))
.replaceWithVoid();
}
@WSReceiver(code = "removeTeamCards", permission = PermLevel.TABLE)
public Uni<Void> removeTeamCards(WebSocketConnection connection, SendTeamCards teamCards) {
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
.chain(c -> cardService.rmTeamCard(c, teamCards.teamUuid(), teamCards.teamName(), teamCards.type))
.invoke(cards -> SSCard.sendRmCards(connection, cards))
.invoke(__ -> SSCard.rmTeamCard(connection, teamCards))
.replaceWithVoid();
}
@RegisterForReflection
public record SendCardAdd(long matchId, long combId, CardModel.CardType type) {
public record SendCardAdd(long matchId, long combId, CardModel.CardType type, String reason) {
}
@RegisterForReflection
public record SendTeamCards(String teamUuid, String teamName, List<CardModel> cards, CardModel.CardType type,
String reason, Date date) {
}
@RegisterForReflection
public record SendTeamCardReturnState(String teamUuid, String teamName, CardModel.CardType type,
int state, Long selectedCategory, Long selectedMatch) {
}
}

View File

@ -117,6 +117,20 @@ public class RState {
}
}
public List<Long> getAllCategories(String uuid) {
return tableStates.values().stream()
.filter(s -> s.getCompetitionUuid()
.equals(uuid) && s.getSelectedCategory() != null && s.getSelectedCategory() != -1)
.map(TableState::getSelectedCategory).distinct().toList();
}
public List<Long> getAllMatchActive(String uuid) {
return tableStates.values().stream()
.filter(s -> s.getCompetitionUuid()
.equals(uuid) && s.getState() == MatchState.IN_PROGRESS && s.getSelectedMatch() != null && s.getSelectedMatch() != -1)
.map(TableState::getSelectedMatch).distinct().toList();
}
@RegisterForReflection
public record ChronoState(long time, long startTime, long configTime, long configPause, int state) {
public boolean isRunning() {

View File

@ -2,15 +2,26 @@ package fr.titionfire.ffsaf.ws.send;
import fr.titionfire.ffsaf.data.model.CardModel;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import fr.titionfire.ffsaf.ws.recv.RCard;
import io.quarkus.websockets.next.WebSocketConnection;
import java.util.List;
public class SSCard {
public static void sendCard(WebSocketConnection connection, CardModel cardModel) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendCard", cardModel);
public static void sendCards(WebSocketConnection connection, List<CardModel> cardModel) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendCards", cardModel);
}
public static void sendRmCard(WebSocketConnection connection, Long id) {
CompetitionWS.sendNotifyToOtherEditor(connection, "rmCard", id);
public static void sendRmCards(WebSocketConnection connection, List<Long> ids) {
CompetitionWS.sendNotifyToOtherEditor(connection, "rmCards", ids);
}
public static void sendTeamCard(WebSocketConnection connection, RCard.SendTeamCards teamCards) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendTeamCard", teamCards);
}
public static void rmTeamCard(WebSocketConnection connection, RCard.SendTeamCards teamCards) {
CompetitionWS.sendNotifyToOtherEditor(connection, "rmTeamCard", teamCards);
}
}

View File

@ -16,10 +16,13 @@
"blue": "Blue",
"cardAdded": "Card added",
"cardRemoved": "Card removed",
"carton": "Card",
"cartonDéquipe": "Team's card",
"cartonJaune": "Yellow card",
"cartonNoir": "Black card",
"cartonRouge": "Red card",
"catégorie": "Category",
"ceCartonEstIssuDunCartonDéquipe": "This card comes from a team card, do you really want to delete it?",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
@ -48,9 +51,12 @@
"confirm3.title": "Change category type",
"confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!",
"confirm4.title": "Delete category",
"confirmer": "Confirm",
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
"contre": "vs",
"couleur": "Color",
"créerLesMatchs": "Create matches",
"date": "Date",
"demi-finalesEtFinales": "Semi-finals and finals",
"duréePause": "Pause duration",
"duréeRound": "Round duration",
@ -72,7 +78,9 @@
"genre.f": "F",
"genre.h": "M",
"genre.na": "NA",
"individuelle": "Individual",
"inscrit": "Registered",
"listeDesCartons": "List of cards",
"manche": "Round",
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
"matches": "Matches",
@ -117,6 +125,9 @@
"team": "Team",
"terminé": "Finished",
"texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.",
"toast.card.team.error": "Error while editing team card",
"toast.card.team.pending": "Editing team card...",
"toast.card.team.success": "Team card edited!",
"toast.createCategory.error": "Error while creating the category",
"toast.createCategory.pending": "Creating category...",
"toast.createCategory.success": "Category created!",

View File

@ -16,10 +16,13 @@
"blue": "Blue",
"cardAdded": "Carton ajouté",
"cardRemoved": "Carton retiré",
"carton": "Carton",
"cartonDéquipe": "Carton d'équipe",
"cartonJaune": "Carton jaune",
"cartonNoir": "Carton noir",
"cartonRouge": "Carton rouge",
"catégorie": "Catégorie",
"ceCartonEstIssuDunCartonDéquipe": "Ce carton est issu d'un carton d'équipe, voulez-vous vraiment le supprimer ?",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
@ -48,9 +51,12 @@
"confirm3.title": "Changement de type de catégorie",
"confirm4.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !",
"confirm4.title": "Suppression de la catégorie",
"confirmer": "Confirmer",
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
"contre": "contre",
"couleur": "Couleur",
"créerLesMatchs": "Créer les matchs",
"date": "Date",
"demi-finalesEtFinales": "Demi-finales et finales",
"duréePause": "Durée pause",
"duréeRound": "Durée round",
@ -72,7 +78,9 @@
"genre.f": "F",
"genre.h": "H",
"genre.na": "NA",
"individuelle": "Individuelle",
"inscrit": "Inscrit",
"listeDesCartons": "Liste des cartons",
"manche": "Manche",
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
"matches": "Matches",
@ -117,6 +125,9 @@
"team": "Équipe",
"terminé": "Terminé",
"texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.",
"toast.card.team.error": "Erreur lors de la modification du carton d'équipe",
"toast.card.team.pending": "Modification du carton d'équipe...",
"toast.card.team.success": "Carton d'équipe modifié !",
"toast.createCategory.error": "Erreur lors de la création de la catégorie",
"toast.createCategory.pending": "Création de la catégorie...",
"toast.createCategory.success": "Catégorie créée !",

View File

@ -1,7 +1,7 @@
import {createContext, useContext, useEffect, useReducer} from "react";
import {useWS} from "./useWS.jsx";
const CardContext = createContext({});
const CardContext = createContext({comb: {}, team: []});
const CardDispatchContext = createContext(() => {
});
@ -29,34 +29,49 @@ export function compareCardOrder(a, b) {
function reducer(state, action) {
switch (action.type) {
case 'SET_CARD':
if (state[action.payload.id] === undefined || !compareCards(action.payload, state[action.payload.id])) {
if (state.comb[action.payload.id] === undefined || !compareCards(action.payload, state.comb[action.payload.id])) {
return {
...state,
comb: {
...state.comb,
[action.payload.id]: action.payload
},
team: state.team
}
}
return state
case 'SET_ALL':
if (action.payload.some(e => state[e.id] === undefined || !compareCards(e, state[e.id]))) {
if (action.payload.some(e => state.comb[e.id] === undefined || !compareCards(e, state.comb[e.id]))) {
const newCombs = {};
for (const o of action.payload) {
newCombs[o.id] = o;
}
return {
...state,
comb: {
...state.comb,
...newCombs
},
team: state.team
}
}
return state
case 'REMOVE_CARD':
console.log("Removing card", action.payload, state[action.payload]);
if (state[action.payload] !== undefined) {
case 'REMOVE_CARDS':
const newState = {...state}
delete newState[action.payload]
for (const id of action.payload)
delete newState.comb[id]
return newState
case 'SET_TEAM_CARD':
return {
comb: state.comb,
team: [...state.team.filter(e => e.teamName !== action.payload.teamName || e.teamUuid !== action.payload.teamUuid || e.type !== action.payload.type),
action.payload]
}
return state
case 'REMOVE_TEAM_CARD':
return {
comb: state.comb,
team: [...state.team.filter(e => e.teamName !== action.payload.teamName || e.teamUuid !== action.payload.teamUuid || e.type !== action.payload.type)]
}
default:
return state
}
@ -66,18 +81,35 @@ function WSListener({dispatch}) {
const {dispatch: dispatchWS} = useWS()
useEffect(() => {
const sendCard = ({data}) => {
dispatch({type: 'SET_CARD', payload: data});
const sendCards = ({data}) => {
dispatch({type: 'SET_ALL', payload: data});
}
const rmCard = ({data}) => {
dispatch({type: 'REMOVE_CARD', payload: data});
const rmCards = ({data}) => {
dispatch({type: 'REMOVE_CARDS', payload: data});
}
const sendTeamCard = ({data}) => {
dispatch({type: 'SET_ALL', payload: data.cards});
dispatch({
type: 'SET_TEAM_CARD',
payload: {teamName: data.teamName, teamUuid: data.teamUuid, type: data.type, reason: data.reason, date: data.date}
});
}
const rmTeamCard = ({data}) => {
dispatch({
type: 'REMOVE_TEAM_CARD',
payload: {teamName: data.teamName, teamUuid: data.teamUuid, type: data.type, reason: data.reason, date: data.date}
});
}
dispatchWS({type: 'addListener', payload: {callback: sendCard, code: 'sendCard'}})
dispatchWS({type: 'addListener', payload: {callback: rmCard, code: 'rmCard'}})
dispatchWS({type: 'addListener', payload: {callback: sendCards, code: 'sendCards'}})
dispatchWS({type: 'addListener', payload: {callback: rmCards, code: 'rmCards'}})
dispatchWS({type: 'addListener', payload: {callback: sendTeamCard, code: 'sendTeamCard'}})
dispatchWS({type: 'addListener', payload: {callback: rmTeamCard, code: 'rmTeamCard'}})
return () => {
dispatchWS({type: 'removeListener', payload: sendCard})
dispatchWS({type: 'removeListener', payload: rmCard})
dispatchWS({type: 'removeListener', payload: sendCards})
dispatchWS({type: 'removeListener', payload: rmCards})
dispatchWS({type: 'removeListener', payload: sendTeamCard})
dispatchWS({type: 'removeListener', payload: rmTeamCard})
}
}, []);
@ -85,7 +117,7 @@ function WSListener({dispatch}) {
}
export function CardsProvider({children}) {
const [cards, dispatch] = useReducer(reducer, {})
const [cards, dispatch] = useReducer(reducer, {comb: {}, team: []})
return <CardContext.Provider value={cards}>
<CardDispatchContext.Provider value={dispatch}>
@ -98,9 +130,9 @@ export function CardsProvider({children}) {
export function useCards() {
const cards = useContext(CardContext);
return {
cards,
cards_v: Object.values(cards),
...useCardsStatic(Object.values(cards))
cards_t: cards.team,
cards_v: Object.values(cards.comb),
...useCardsStatic(Object.values(cards.comb))
}
}

View File

@ -29,7 +29,7 @@ function reducer(state, action) {
//console.debug("Updating comb", comb);
return {
...state,
[comb.id]: comb
[comb.id]: {...state[comb.id], ...comb}
}
}
return state
@ -49,7 +49,10 @@ function reducer(state, action) {
if (combs.some(e => state[e.id] === undefined || !compareCombs(e, state[e.id]))) {
const newCombs = {};
for (const o of combs) {
newCombs[o.id] = o;
newCombs[o.id] = {
...state[o.id],
...o
};
}
//console.debug("Updating combs", newCombs);

View File

@ -88,7 +88,7 @@ export function CompetitionRegisterAdmin({source}) {
<div className="col-lg-9">
{data ? <div className="">
<MakeCentralPanel
data={state.filter(s => (clubFilter.length === 0 || s.data.club.name === clubFilter)
data={state.filter(s => (clubFilter.length === 0 || s.data.club?.name === clubFilter)
&& (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter)
&& (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter))
&& (!filterNotWeight || (data3?.requiredWeight.includes(s.data.categorie) && (

View File

@ -11,12 +11,14 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
import {faGlobe, faTableCellsLarge} from "@fortawesome/free-solid-svg-icons";
import {faFile, faGlobe, faTableCellsLarge, faTrash} from "@fortawesome/free-solid-svg-icons";
import {Trans, useTranslation} from "react-i18next";
import i18n from "i18next";
import {getToastMessage} from "../../../utils/Tools.js";
import {copyStyles} from "../../../utils/copyStyles.js";
import {StateWindow} from "./StateWindow.jsx";
import {CombName, useCombs} from "../../../hooks/useComb.jsx";
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
const vite_url = import.meta.env.VITE_URL;
@ -169,6 +171,7 @@ function Menu({menuActions, compUuid}) {
const e = document.getElementById("actionMenu")
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
const teamCardModal = useRef(null);
const {t} = useTranslation("cm");
const [showStateWin, setShowStateWin] = useState(false)
@ -235,6 +238,8 @@ function Menu({menuActions, compUuid}) {
if (button === "obs") {
downloadResourcesAsZip(menuActions.current.resourceList || [])
.then(__ => console.log("Ressources téléchargées"));
} else if (button === "cards") {
teamCardModal.current.click();
}
}
}
@ -274,8 +279,14 @@ function Menu({menuActions, compUuid}) {
{createPortal(
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faFile} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("cards")}
onMouseUp={() => longPressUp("cards")}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t("carton")}/>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
style={{color: "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("obs")}
onMouseUp={() => longPressUp("obs")}
data-bs-toggle="tooltip2" data-bs-placement="top"
@ -347,10 +358,147 @@ function Menu({menuActions, compUuid}) {
</div>
</div>
</div>
<button ref={teamCardModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#TeamCardModal"
style={{display: 'none'}}>
Launch OBS Modal
</button>
<div className="modal modal-xl fade" id="TeamCardModal" tabIndex="-1" aria-labelledby="TeamCardModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<TeamCardModal/>
</div>
</div>
</div>
</>
}
function CategoryHeader({cat, setCatId}) {
function TeamCardModal() {
const [club, setClub] = useState("")
const {t} = useTranslation("cm");
const {combs} = useCombs()
const {sendRequest} = useWS()
const {cards_t, cards_v} = useCards()
const cardDispatch = useCardsDispatch();
const {data} = useRequestWS("getAllForTeamNoDetail", {}, null);
useEffect(() => {
if (!data)
return;
for (const card of data) {
cardDispatch({type: 'SET_TEAM_CARD', payload: card});
}
}, [data]);
let clubList = [];
if (combs != null) {
clubList = Object.values(combs).map(d => d.club_str).filter((v, i, a) => v !== "" && v !== undefined && a.indexOf(v) === i);
}
const handleAdd = (e) => {
e.preventDefault();
toast.promise(sendRequest("applyTeamCards", {
teamUuid: Object.values(combs).find(d => d.club_str === club)?.club_uuid,
teamName: club,
type: "YELLOW"
}),
getToastMessage("toast.card.team", "cm"))
.then(() => {
})
}
const GetCard = ({type}) => {
if (!type)
return <></>
let bg = "";
switch (type) {
case "YELLOW":
bg = " bg-warning";
break;
case "RED":
bg = " bg-danger";
break;
case "BLACK":
bg = " bg-dark text-white";
break;
case "BLUE":
bg = " bg-primary text-white";
break;
}
return <span className={"badge border border-light p-2" + bg}><span className="visually-hidden">card</span></span>
}
let cards = [...cards_t, ...cards_v].sort((a, b) => new Date(b.date) - new Date(a.date))
return <>
<div className="modal-header">
<h5 className="modal-title">{t('carton')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<h5>{t('cartonDéquipe')}</h5>
<div className="input-group mb-3">
<label htmlFor="inputGroupSelect09" className="input-group-text">{t('club')}</label>
<select id="inputGroupSelect09" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
{clubList.sort((a, b) => a.localeCompare(b)).map((club, index) => (
<option key={index} value={club}>{club}</option>))}
</select>
<button className="btn btn-outline-primary" type="button" onClick={handleAdd}>{t("ajouter")}</button>
</div>
<h5>{t('listeDesCartons')}</h5>
<table className="table">
<thead>
<tr>
<th scope="col">{t('date')}</th>
<th scope="col">{t('type')}</th>
<th scope="col">{t('couleur')}</th>
<th scope="col">{t('nom')}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{cards.map((card, index) => <tr key={index}>
<td scope="row">{new Date(card.date).toLocaleString()}</td>
{card.teamName ? <>
<td>{t('team')}</td>
<td><GetCard type={card.type}/></td>
<td>{card.teamName}</td>
</> : <>
<td>{card.teamCard ? "|-> " + t('team'): t('individuelle')} </td>
<td><GetCard type={card.type}/></td>
<td><CombName combId={card.comb}/></td>
</>}
<td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => {
if (confirm("Êtes-vous sûr de vouloir supprimer ce carton ?")) {
if (card.teamName) {
toast.promise(sendRequest("removeTeamCards", {
teamUuid: card.teamUuid, teamName: card.teamName, type: card.type
}),
getToastMessage("toast.card.team", "cm"))
.then(() => {
})
} else {
sendRequest('sendCardRm', {matchId: card.match, combId: card.comb, type: card.type})
.then(() => toast.success(t('cardRemoved')))
.catch(err => toast.error(err))
}
}
}}><FontAwesomeIcon icon={faTrash}/></td>
</tr>)}
</tbody>
</table>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
</div>
</>
}
function CategoryHeader({
cat, setCatId
}) {
const setLoading = useLoadingSwitcher()
const bthRef = useRef();
const confirmRef = useRef();
@ -428,7 +576,7 @@ function CategoryHeader({cat, setCatId}) {
<button className="btn btn-primary float-end" onClick={() => {
setModal(cat);
bthRef.current.click();
}} disabled={cat === null}>Modifier
}} disabled={cat === null}>{t('modifier')}
</button>
</div>

View File

@ -197,7 +197,7 @@ function MatchList({matches, cat, menuActions}) {
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v), end: m.end}))
.map(m => ({...m, ...win_end(m, cards_v)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => {

View File

@ -1,11 +1,11 @@
import React, {useEffect, useRef, useState} from "react";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {createPortal} from "react-dom";
import {copyStyles} from "../../../utils/copyStyles.js";
import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx";
import {faArrowRightArrowLeft, faDisplay} from "@fortawesome/free-solid-svg-icons";
import {faArrowRightArrowLeft, faDisplay, faFile, faTrash} from "@fortawesome/free-solid-svg-icons";
import {PubAffWindow} from "./PubAffWindow.jsx";
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
import {ChronoPanel} from "./CMTChronoPanel.jsx";
@ -13,8 +13,10 @@ import {CategorieSelect} from "./CMTMatchPanel.jsx";
import {PointPanel} from "./CMTPoint.jsx";
import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import {toast} from "react-toastify";
import {Flip, toast} from "react-toastify";
import {useTranslation} from "react-i18next";
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {getToastMessage} from "../../../utils/Tools.js";
export function CMTable() {
const combDispatch = useCombsDispatch()
@ -78,6 +80,7 @@ function Menu({menuActions}) {
const {connected, connect, disconnect} = useOBS();
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
const teamCardModal = useRef(null);
const {t} = useTranslation("cm");
const externalWindow = useRef(null)
@ -194,6 +197,9 @@ function Menu({menuActions}) {
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<span onClick={() => obsModal.current.click()} style={{cursor: "pointer"}}>Zone {zone}</span>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faFile} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={() => teamCardModal.current.click()} data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('cartonDéquipe')}/>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('ttm.table.inverserLaPosition')}/>
@ -241,10 +247,67 @@ function Menu({menuActions}) {
</div>
</div>
</div>
<button ref={teamCardModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#TeamCardModal"
style={{display: 'none'}}>
Launch OBS Modal
</button>s
<div className="modal fade" id="TeamCardModal" tabIndex="-1" aria-labelledby="TeamCardModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<TeamCardModal/>
</div>
</div>
</div>
<SendLiceName name={zone}/>
</>
}
function TeamCardModal() {
const [club, setClub] = useState("")
const {t} = useTranslation("cm");
const {combs} = useCombs()
const {sendRequest} = useWS()
let clubList = [];
if (combs != null) {
clubList = Object.values(combs).map(d => d.club_str).filter((v, i, a) => v !== "" && v !== undefined && a.indexOf(v) === i);
}
const handleAdd = (e) => {
e.preventDefault();
toast.promise(sendRequest("applyTeamCards", {
teamUuid: Object.values(combs).find(d => d.club_str === club)?.club_uuid,
teamName: club,
type: "YELLOW"
}),
getToastMessage("toast.card.team", "cm"))
.then(() => {
})
}
return <>
<div className="modal-header">
<h5 className="modal-title">{t('cartonDéquipe')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="input-group mb-3">
<label htmlFor="inputGroupSelect09" className="input-group-text">{t('club')}</label>
<select id="inputGroupSelect09" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
{clubList.sort((a, b) => a.localeCompare(b)).map((club, index) => (
<option key={index} value={club}>{club}</option>))}
</select>
<button className="btn btn-outline-primary" type="button" onClick={handleAdd}>{t("ajouter")}</button>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
</div>
</>
}
function SendLiceName({name}) {
const {sendNotify, setState} = useWS();
@ -258,15 +321,94 @@ function SendLiceName({name}) {
}
function SendCatId({catId}) {
const notifState = useRef(undefined);
const {sendNotify, setState, dispatch, tableState} = useWS();
const {t} = useTranslation("cm");
useEffect(() => {
const welcomeInfo = () => {
sendNotify("sendState", tableState.current)
}
const sendTeamCards = ({data}) => {
function content({closeToast, data}) {
const sendState = (s) => {
sendNotify("sendTeamCardReturnState", {
state: s,
teamUuid: data.teamUuid,
teamName: data.teamName,
type: data.type,
selectedCategory: tableState.current.selectedCategory,
selectedMatch: tableState.current.selectedMatch
});
}
return (
<div className="flex items-center w-full">
<span>{`Un carton jaune d'équipe a été émis à l'encontre du club ${data.teamName}. Dans votre zone de combat :`}</span><br/>
<div className="form-check">
<input className="form-check-input" type="radio" name="radioState" id="radioState1"
onChange={e => e.target.checked ? notifState.current = 0 : null}/>
<label className="form-check-label" htmlFor="radioState1">
Rien n'est en cours
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="radio" name="radioState" id="radioState2"
onChange={e => e.target.checked ? notifState.current = 1 : null}/>
<label className="form-check-label" htmlFor="radioState2">
La categorie est en cours
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="radio" name="radioState" id="radioState3"
onChange={e => e.target.checked ? notifState.current = 2 : null}/>
<label className="form-check-label" htmlFor="radioState3">
Le match est en cours
</label>
</div>
<button className="btn btn-sm btn-outline-primary ml-2" onClick={() => {
if (notifState.current === undefined) {
return;
}
sendState(notifState.current)
closeToast(true);
}}>
{t('confirmer')}
</button>
</div>
);
}
toast.warn(content, {
position: "top-center",
autoClose: false,
closeButton: false,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: false,
progress: undefined,
theme: "colored",
transition: Flip,
data: {
teamUuid: data.teamUuid,
teamName: data.teamName,
type: data.type,
}
});
}
welcomeInfo();
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
return () => dispatch({type: 'removeListener', payload: welcomeInfo});
dispatch({type: 'addListener', payload: {callback: sendTeamCards, code: 'sendTeamCards'}})
return () => {
dispatch({type: 'removeListener', payload: welcomeInfo});
dispatch({type: 'removeListener', payload: sendTeamCards});
setState({selectedCategory: -1});
sendNotify("sendSelectCategory", -1);
}
}, []);
useEffect(() => {

View File

@ -694,6 +694,7 @@ function MatchList({matches, cat, groups, reducer}) {
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>

View File

@ -267,7 +267,27 @@ function CardPanel({matchId, match, vEnd, admin}) {
})
}
const handleCardRm = (combId, type) => {
const confirmRm = (combId, type) => {
function content({closeToast}) {
return (
<div className="flex items-center w-full">
<span>{t('ceCartonEstIssuDunCartonDéquipe')}</span>{' '}
<button
className="btn btn-sm btn-warning ml-2"
onClick={() => {
closeToast(true);
handleCardRm_(combId, type)
}}>
{t('confirmer')}
</button>
</div>
);
}
toast.warn(content);
}
const handleCardRm_ = (combId, type) => {
setLoading(1)
sendRequest('sendCardRm', {matchId, combId, type})
.then(() => {
@ -282,6 +302,15 @@ function CardPanel({matchId, match, vEnd, admin}) {
}
const cards = getCardInMatch(match)
const handleCardRm = (combId, type) => {
if (cards.find(c => c.comb === combId && c.type === type)?.teamCard) {
confirmRm(combId, type)
} else {
handleCardRm_(combId, type)
}
}
const MakeList = ({comb}) => {
const card = getHeightCardForCombInMatch(comb, match);