diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java index 92d192a..f3b0c2c 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java @@ -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, diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/ClubCardModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/ClubCardModel.java new file mode 100644 index 0000000..1831714 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/ClubCardModel.java @@ -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 cardIds; +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/ClubCardRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/ClubCardRepository.java new file mode 100644 index 0000000..bc7e08e --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/ClubCardRepository.java @@ -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 { + +} \ No newline at end of file diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java index bf46bdf..e96fd80 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java @@ -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 extractCombIds(MatchEntity match) { - List 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> 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> 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 newCards = new ArrayList<>(); + for (Long id : combIds) { + Optional 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> 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> 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)))); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java index 657b6db..f7b96fa 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java @@ -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> 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 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 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 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 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 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) { } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java index 047dd94..88c3ffb 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java @@ -117,6 +117,20 @@ public class RState { } } + public List 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 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() { diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java index 51dec9d..545dccd 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java @@ -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) { + 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 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); } } diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index a805338..0ac10d4 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -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!", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index a0f1e20..5ed39bf 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -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 !", diff --git a/src/main/webapp/src/hooks/useCard.jsx b/src/main/webapp/src/hooks/useCard.jsx index ce5596e..c5882fb 100644 --- a/src/main/webapp/src/hooks/useCard.jsx +++ b/src/main/webapp/src/hooks/useCard.jsx @@ -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, - [action.payload.id]: action.payload + 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, - ...newCombs + 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) { - const newState = {...state} - delete newState[action.payload] - return newState + case 'REMOVE_CARDS': + const newState = {...state} + 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 @@ -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)) } } diff --git a/src/main/webapp/src/hooks/useComb.jsx b/src/main/webapp/src/hooks/useComb.jsx index 92a3936..44c08a3 100644 --- a/src/main/webapp/src/hooks/useComb.jsx +++ b/src/main/webapp/src/hooks/useComb.jsx @@ -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); diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index 93c8969..d018190 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -88,7 +88,7 @@ export function CompetitionRegisterAdmin({source}) {
{data ?
(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) && ( diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 92ecf3c..0336396 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -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( <>
+ longPressDown("cards")} + onMouseUp={() => longPressUp("cards")} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title={t("carton")}/> longPressDown("obs")} onMouseUp={() => longPressUp("obs")} data-bs-toggle="tooltip2" data-bs-placement="top" @@ -347,10 +358,147 @@ function Menu({menuActions, compUuid}) {
+ + + } -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 card + } + + let cards = [...cards_t, ...cards_v].sort((a, b) => new Date(b.date) - new Date(a.date)) + return <> +
+
{t('carton')}
+ +
+
+
{t('cartonDéquipe')}
+
+ + + +
+ +
{t('listeDesCartons')}
+ + + + + + + + + + + + {cards.map((card, index) => + + {card.teamName ? <> + + + + : <> + + + + } + + + )} + +
{t('date')}{t('type')}{t('couleur')}{t('nom')}
{new Date(card.date).toLocaleString()}{t('team')}{card.teamName}{card.teamCard ? "|-> " + t('team'): t('individuelle')} { + 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)) + } + } + }}>
+
+
+ +
+ +} + +function CategoryHeader({ + cat, setCatId + }) { const setLoading = useLoadingSwitcher() const bthRef = useRef(); const confirmRef = useRef(); @@ -428,7 +576,7 @@ function CategoryHeader({cat, setCatId}) { diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 1f1055e..1e6aebd 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -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) => { diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 63bd969..717510b 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -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}) {
obsModal.current.click()} style={{cursor: "pointer"}}>Zone {zone}
+ teamCardModal.current.click()} data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title={t('cartonDéquipe')}/> @@ -241,10 +247,67 @@ function Menu({menuActions}) { + + s + } +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 <> +
+
{t('cartonDéquipe')}
+ +
+
+
+ + + +
+
+
+ +
+ +} + 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 ( +
+ {`Un carton jaune d'équipe a été émis à l'encontre du club ${data.teamName}. Dans votre zone de combat :`}
+
+ e.target.checked ? notifState.current = 0 : null}/> + +
+
+ e.target.checked ? notifState.current = 1 : null}/> + +
+
+ e.target.checked ? notifState.current = 2 : null}/> + +
+ +
+ ); + } + + 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(() => { diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 6f67a51..cfe47b5 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -694,6 +694,7 @@ function MatchList({matches, cat, groups, reducer}) { + diff --git a/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx b/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx index a2e58e8..be41709 100644 --- a/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx @@ -267,7 +267,27 @@ function CardPanel({matchId, match, vEnd, admin}) { }) } - const handleCardRm = (combId, type) => { + const confirmRm = (combId, type) => { + function content({closeToast}) { + return ( +
+ {t('ceCartonEstIssuDunCartonDéquipe')}{' '} + +
+ ); + } + + 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);