From 189eb135bb77d9dbee1871ea26534ae44fcbc984 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 26 Jan 2026 22:36:26 +0100 Subject: [PATCH 1/8] wip: competition card system --- .../ffsaf/data/model/CardModel.java | 37 ++ .../ffsaf/data/repository/CardRepository.java | 10 + .../ffsaf/domain/service/CardService.java | 91 +++++ .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 4 +- .../fr/titionfire/ffsaf/ws/recv/RCard.java | 102 +++++ .../titionfire/ffsaf/ws/recv/RCardboard.java | 129 ------ .../titionfire/ffsaf/ws/recv/RCategorie.java | 8 + .../fr/titionfire/ffsaf/ws/send/SSCard.java | 16 + .../titionfire/ffsaf/ws/send/SSCardboard.java | 12 - .../resources/lang/messages_en.properties | 2 + .../resources/lang/messages_fr.properties | 4 +- src/main/webapp/public/locales/en/cm.json | 10 + src/main/webapp/public/locales/en/common.json | 128 +++--- src/main/webapp/public/locales/fr/cm.json | 10 + src/main/webapp/public/locales/fr/common.json | 136 +++---- src/main/webapp/src/hooks/useCard.jsx | 135 ++++++ src/main/webapp/src/hooks/useComb.jsx | 2 +- .../competition/editor/CMTMatchPanel.jsx | 368 +++-------------- .../editor/CategoryAdminContent.jsx | 112 ++++- .../editor/CompetitionManagerRoot.jsx | 17 +- .../competition/editor/ScoreAndCardPanel.jsx | 384 ++++++++++++++++++ .../webapp/src/pages/result/DrawGraph.jsx | 48 ++- src/main/webapp/src/utils/CompetitionTools.js | 14 +- src/main/webapp/src/utils/Tools.js | 69 +++- 24 files changed, 1232 insertions(+), 616 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/CardRepository.java create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java delete mode 100644 src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java delete mode 100644 src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java create mode 100644 src/main/webapp/src/hooks/useCard.jsx create mode 100644 src/main/webapp/src/pages/competition/editor/ScoreAndCardPanel.jsx diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java new file mode 100644 index 0000000..92d192a --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java @@ -0,0 +1,37 @@ +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; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "card") +public class CardModel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + Long comb; + Long match; + Long category; + Long competition; + + CardType type; + + public enum CardType { + BLUE, + YELLOW, + RED, + BLACK + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CardRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CardRepository.java new file mode 100644 index 0000000..a765271 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/CardRepository.java @@ -0,0 +1,10 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.CardModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CardRepository 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 new file mode 100644 index 0000000..bf46bdf --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java @@ -0,0 +1,91 @@ +package fr.titionfire.ffsaf.domain.service; + +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.domain.entity.MatchEntity; +import fr.titionfire.ffsaf.rest.exception.DBadRequestException; +import fr.titionfire.ffsaf.ws.recv.RCard; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.panache.common.Sort; +import io.smallrye.mutiny.Uni; +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; + +@WithSession +@ApplicationScoped +public class CardService { + @Inject + CardRepository cardRepository; + + @Inject + TradService trad; + + private static final List COMPETITION_LEVEL_CARDS = List.of( + CardModel.CardType.YELLOW, + CardModel.CardType.RED, + CardModel.CardType.BLACK + ); + + private List extractCombIds(MatchModel match) { + List ids = new ArrayList<>(); + if (match.getC1_id() != null) + ids.add(match.getC1_id().getId()); + if (match.getC2_id() != null) + ids.add(match.getC2_id().getId()); + if (match.getC1_guest() != null) + ids.add(match.getC1_guest().getId() * -1); + if (match.getC2_guest() != null) + ids.add(match.getC2_guest().getId() * -1); + 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", + match.getCategory().getCompet().getId(), COMPETITION_LEVEL_CARDS, + extractCombIds(match), match.getCategory().getId()); + } + + public Uni> getAll(Long competitionId) { + return cardRepository.list("competition = ?1", competitionId); + } + + public Uni checkCanBeAdded(RCard.SendCardAdd card, MatchModel matchModel) { + return cardRepository.find("competition = ?1 AND comb = ?2", + Sort.descending("type"), + matchModel.getCategory().getCompet().getId(), card.combId()) + .firstResult() + .map(card_ -> { + if (card.type() == CardModel.CardType.BLUE) { + return card_ == null || (card_.getType() == CardModel.CardType.BLUE + && !Objects.equals(card_.getCategory(), matchModel.getCategory().getId())); + } + if (card.type() == CardModel.CardType.BLACK) { + return card_ != null && card_.getType() == CardModel.CardType.RED; + } + + return card_ == null || card_.getType().ordinal() < card.type().ordinal(); + }) + .chain(b -> { + if (b) + return Uni.createFrom().item(card); + else + return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added"))); + }); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index e346295..7978fbd 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -46,7 +46,7 @@ public class CompetitionWS { RRegister rRegister; @Inject - RCardboard rCardboard; + RCard rCard; @Inject RTeam rTeam; @@ -93,7 +93,7 @@ public class CompetitionWS { getWSReceiverMethods(RMatch.class, rMatch); getWSReceiverMethods(RCategorie.class, rCategorie); getWSReceiverMethods(RRegister.class, rRegister); - getWSReceiverMethods(RCardboard.class, rCardboard); + getWSReceiverMethods(RCard.class, rCard); getWSReceiverMethods(RTeam.class, rTeam); executor = notifyExecutor; diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java new file mode 100644 index 0000000..c061c71 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java @@ -0,0 +1,102 @@ +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.MatchRepository; +import fr.titionfire.ffsaf.domain.service.CardService; +import fr.titionfire.ffsaf.domain.service.TradService; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.rest.exception.DNotFoundException; +import fr.titionfire.ffsaf.ws.PermLevel; +import fr.titionfire.ffsaf.ws.send.SSCard; +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 io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.List; + +@WithSession +@ApplicationScoped +@RegisterForReflection +public class RCard { + + @Inject + MatchRepository matchRepository; + + @Inject + CardRepository cardRepository; + + @Inject + CardService cardService; + + @Inject + CombRepository combRepository; + + @Inject + CompetitionGuestRepository competitionGuestRepository; + + @Inject + TradService trad; + + private Uni getById(long id, WebSocketConnection connection) { + return matchRepository.findById(id) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException(trad.t("matche.non.trouver")); + if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) + throw new DForbiddenException(trad.t("permission.denied")); + })); + } + + @WSReceiver(code = "getCardForMatch", permission = PermLevel.VIEW) + public Uni> getCardForMatch(WebSocketConnection connection, Long matchId) { + return getById(matchId, connection).chain(matchModel -> cardService.getForMatch(matchModel)); + } + + @WSReceiver(code = "sendCardAdd", permission = PermLevel.TABLE) + public Uni sendCardAdd(WebSocketConnection connection, SendCardAdd card) { + return getById(card.matchId(), connection) + .chain(matchModel -> cardService.checkCanBeAdded(card, matchModel) + .chain(c -> { + CardModel model = new CardModel(); + model.setComb(card.combId()); + model.setMatch(card.matchId()); + model.setCategory(matchModel.getCategory().getId()); + model.setCompetition(matchModel.getCategory().getCompet().getId()); + model.setType(card.type()); + + return Panache.withTransaction(() -> cardRepository.persist(model)); + }) + ) + .invoke(cardModel -> SSCard.sendCard(connection, cardModel)) + .replaceWithVoid(); + } + + @WSReceiver(code = "sendCardRm", permission = PermLevel.ADMIN) + public Uni sendCardRm(WebSocketConnection connection, SendCardAdd card) { + return getById(card.matchId(), connection) + .chain(matchModel -> cardRepository.find("match = ?1 AND comb = ?2 AND type = ?3", + matchModel.getId(), card.combId(), card.type()) + .firstResult() + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException(trad.t("carton.non.trouver")); + SSCard.sendRmCard(connection, o.getId()); + })) + .chain(cardModel -> Panache.withTransaction(() -> cardRepository.delete(cardModel))) + ) + .replaceWithVoid(); + } + + @RegisterForReflection + public record SendCardAdd(long matchId, long combId, CardModel.CardType type) { + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java deleted file mode 100644 index aefc5b7..0000000 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java +++ /dev/null @@ -1,129 +0,0 @@ -package fr.titionfire.ffsaf.ws.recv; - -import fr.titionfire.ffsaf.data.model.CardboardModel; -import fr.titionfire.ffsaf.data.model.MatchModel; -import fr.titionfire.ffsaf.data.repository.CardboardRepository; -import fr.titionfire.ffsaf.data.repository.MatchRepository; -import fr.titionfire.ffsaf.domain.entity.CardboardEntity; -import fr.titionfire.ffsaf.domain.service.TradService; -import fr.titionfire.ffsaf.rest.exception.DForbiddenException; -import fr.titionfire.ffsaf.rest.exception.DNotFoundException; -import fr.titionfire.ffsaf.ws.PermLevel; -import fr.titionfire.ffsaf.ws.send.SSCardboard; -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 io.smallrye.mutiny.unchecked.Unchecked; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import lombok.Data; - -import java.util.Objects; - -@WithSession -@ApplicationScoped -@RegisterForReflection -public class RCardboard { - - @Inject - MatchRepository matchRepository; - - @Inject - CardboardRepository cardboardRepository; - - @Inject - TradService trad; - - private Uni getById(long id, WebSocketConnection connection) { - return matchRepository.findById(id) - .invoke(Unchecked.consumer(o -> { - if (o == null) - throw new DNotFoundException(trad.t("matche.non.trouver")); - if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException(trad.t("permission.denied")); - })); - } - - @WSReceiver(code = "sendCardboardChange", permission = PermLevel.TABLE) - public Uni sendCardboardChange(WebSocketConnection connection, SendCardboard card) { - return getById(card.matchId, connection) - .chain(matchModel -> cardboardRepository.find("(comb.id = ?1 OR guestComb.id = ?2) AND match.id = ?3", - card.combId, card.combId * -1, card.matchId).firstResult() - .chain(model -> { - if (model != null) { - model.setRed(model.getRed() + card.red); - model.setYellow(model.getYellow() + card.yellow); - return Panache.withTransaction(() -> cardboardRepository.persist(model)); - } - CardboardModel cardboardModel = new CardboardModel(); - - cardboardModel.setCompet(matchModel.getCategory().getCompet()); - cardboardModel.setMatch(matchModel); - cardboardModel.setRed(card.red); - cardboardModel.setYellow(card.yellow); - cardboardModel.setComb(null); - cardboardModel.setGuestComb(null); - - if (card.combId >= 0) { - if (matchModel.getC1_id() != null && matchModel.getC1_id().getId() == card.combId) - cardboardModel.setComb(matchModel.getC1_id()); - if (matchModel.getC2_id() != null && matchModel.getC2_id().getId() == card.combId) - cardboardModel.setComb(matchModel.getC2_id()); - } else { - if (matchModel.getC1_guest() != null && matchModel.getC1_guest() - .getId() == card.combId * -1) - cardboardModel.setGuestComb(matchModel.getC1_guest()); - if (matchModel.getC2_guest() != null && matchModel.getC2_guest() - .getId() == card.combId * -1) - cardboardModel.setGuestComb(matchModel.getC2_guest()); - } - - if (cardboardModel.getComb() == null && cardboardModel.getGuestComb() == null) - return Uni.createFrom().nullItem(); - return Panache.withTransaction(() -> cardboardRepository.persist(cardboardModel)); - })) - .invoke(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model))) - .replaceWithVoid(); - } - - @WSReceiver(code = "getCardboardWithoutThis", permission = PermLevel.VIEW) - public Uni getCardboardWithoutThis(WebSocketConnection connection, Long matchId) { - return getById(matchId, connection) - .chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2", - matchModel.getCategory().getCompet(), matchModel) - .map(models -> { - CardboardAllMatch out = new CardboardAllMatch(); - - for (CardboardModel c : models) { - if ((matchModel.getC1_id() != null && Objects.equals(c.getComb(), - matchModel.getC1_id())) || (matchModel.getC1_guest() != null && Objects.equals( - c.getGuestComb(), matchModel.getC1_guest()))) { - out.c1_yellow += c.getYellow(); - out.c1_red += c.getRed(); - } - if ((matchModel.getC2_id() != null && Objects.equals(c.getComb(), - matchModel.getC2_id())) || (matchModel.getC2_guest() != null && Objects.equals( - c.getGuestComb(), matchModel.getC2_guest()))) { - out.c2_yellow += c.getYellow(); - out.c2_red += c.getRed(); - } - } - return out; - })); - } - - @RegisterForReflection - public record SendCardboard(long matchId, long combId, int yellow, int red) { - } - - @Data - @RegisterForReflection - public static class CardboardAllMatch { - int c1_yellow = 0; - int c1_red = 0; - int c2_yellow = 0; - int c2_red = 0; - } -} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index 73502ca..c6088b9 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -1,11 +1,13 @@ package fr.titionfire.ffsaf.ws.recv; +import fr.titionfire.ffsaf.data.model.CardModel; import fr.titionfire.ffsaf.data.model.CategoryModel; import fr.titionfire.ffsaf.data.model.MatchModel; import fr.titionfire.ffsaf.data.model.TreeModel; import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.domain.entity.MatchEntity; import fr.titionfire.ffsaf.domain.entity.TreeEntity; +import fr.titionfire.ffsaf.domain.service.CardService; import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; @@ -47,6 +49,9 @@ public class RCategorie { @Inject CardboardRepository cardboardRepository; + @Inject + CardService cardService; + @Inject TradService trad; @@ -84,6 +89,8 @@ public class RCategorie { .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId()) .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) .invoke(fullCategory::setTrees)) + .call(cat -> cardService.getAll(cat.getCompet().getId()) + .invoke(fullCategory::setCards)) .map(__ -> fullCategory); } @@ -241,5 +248,6 @@ public class RCategorie { String liceName; List trees = null; List matches; + List cards; } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java new file mode 100644 index 0000000..51dec9d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java @@ -0,0 +1,16 @@ +package fr.titionfire.ffsaf.ws.send; + +import fr.titionfire.ffsaf.data.model.CardModel; +import fr.titionfire.ffsaf.ws.CompetitionWS; +import io.quarkus.websockets.next.WebSocketConnection; + +public class SSCard { + + public static void sendCard(WebSocketConnection connection, CardModel cardModel) { + CompetitionWS.sendNotifyToOtherEditor(connection, "sendCard", cardModel); + } + + public static void sendRmCard(WebSocketConnection connection, Long id) { + CompetitionWS.sendNotifyToOtherEditor(connection, "rmCard", id); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java deleted file mode 100644 index c7b17d1..0000000 --- a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java +++ /dev/null @@ -1,12 +0,0 @@ -package fr.titionfire.ffsaf.ws.send; - -import fr.titionfire.ffsaf.domain.entity.CardboardEntity; -import fr.titionfire.ffsaf.ws.CompetitionWS; -import io.quarkus.websockets.next.WebSocketConnection; - -public class SSCardboard { - - public static void sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) { - CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity); - } -} diff --git a/src/main/resources/lang/messages_en.properties b/src/main/resources/lang/messages_en.properties index 262334f..76556ed 100644 --- a/src/main/resources/lang/messages_en.properties +++ b/src/main/resources/lang/messages_en.properties @@ -85,3 +85,5 @@ licence.membre.n.1.inconnue=License member no. 1 unknown licence.membre.n.2.inconnue=License member no. 2 unknown licence.membre.n.3.inconnue=License member no. 3 unknown demande.d.affiliation.non.trouve=Affiliation request not found +carton.non.trouver=Card not found +card.cannot.be.added=Unable to add the card diff --git a/src/main/resources/lang/messages_fr.properties b/src/main/resources/lang/messages_fr.properties index a24c844..d430dfd 100644 --- a/src/main/resources/lang/messages_fr.properties +++ b/src/main/resources/lang/messages_fr.properties @@ -80,4 +80,6 @@ affiliation.deja.existante=Affiliation d licence.membre.n.1.inconnue=Licence du membre n°1 inconnue licence.membre.n.2.inconnue=Licence du membre n°2 inconnue licence.membre.n.3.inconnue=Licence du membre n°3 inconnue -demande.d.affiliation.non.trouve=Demande d'affiliation introuvable \ No newline at end of file +demande.d.affiliation.non.trouve=Demande d'affiliation introuvable +carton.non.trouver=Carton introuvable +card.cannot.be.added=Impossible d'ajouter le carton \ No newline at end of file diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index c993b44..dbb547d 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -5,13 +5,21 @@ "actuel": "Current", "administration": "Administration", "adresseDuServeur": "Server address", + "advertisement": "", "ajouter": "Add", "ajouterDesCombattants": "Add fighters", + "ajouterUn": "Add one", "ajouterUneTeam": "Add team", "attention": "Warning", "aucuneConfigurationObs": "No OBS configuration found, please import one", + "avertissement": "Warning", "bleu": "Blue", "blue": "Blue", + "cardAdded": "Card added", + "cardRemoved": "Card removed", + "cartonJaune": "Yellow card", + "cartonNoir": "Black card", + "cartonRouge": "Red card", "catégorie": "Category", "chrono.+/-...S": "+/- ... s", "chrono.+10S": "+10 s", @@ -47,6 +55,7 @@ "duréePause": "Pause duration", "duréeRound": "Round duration", "editionDeLaCatégorie": "Edit category", + "editionDuMatch": "Match edition", "enregister": "Save", "enregistrer": "Save", "epéeBouclier": "Sword and shield", @@ -100,6 +109,7 @@ "serveur": "Server", "suivant": "Next", "supprimer": "Delete", + "supprimerUn": "Delete one", "sélectionneLesModesDaffichage": "Select display modes", "sélectionner": "Select", "team": "Team", diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json index f7f7f12..33372fe 100644 --- a/src/main/webapp/public/locales/en/common.json +++ b/src/main/webapp/public/locales/en/common.json @@ -247,6 +247,9 @@ "comp.toast.register.self.del.error": "Error during unregistration", "comp.toast.register.self.del.pending": "Unregistration in progress", "comp.toast.register.self.del.success": "Unregistration completed", + "comp.toast.registers.addMultiple.error": "Import failed", + "comp.toast.registers.addMultiple.pending": "Import in progress", + "comp.toast.registers.addMultiple.success": "Import completed successfully 🎉", "comp.toast.save.error": "Failed to save competition", "comp.toast.save.pending": "Saving competition in progress", "comp.toast.save.success": "Competition saved successfully 🎉", @@ -309,6 +312,66 @@ "f": "F", "faitPar": "Done by", "femme": "Female", + "fileImport.variants": { + "categorie": [ + "category", + "catégorie", + "weight category", + "age category" + ], + "club": [ + "club", + "club name", + "association", + "association name" + ], + "genre": [ + "gender", + "genre", + "sex", + "civility" + ], + "licence": [ + "license", + "licence", + "license number", + "license ID", + "ID license", + "licence no" + ], + "nom": [ + "last name", + "nom", + "family name", + "surname", + "lastname" + ], + "overCategory": [ + "over category", + "surclassement", + "category override", + "over classification" + ], + "pays": [ + "country", + "pays", + "country of residence", + "origin country" + ], + "prenom": [ + "first name", + "prénom", + "given name", + "first given name" + ], + "weight": [ + "weight", + "poids", + "weight (kg)", + "actual weight", + "mass" + ] + }, "filtre": "Filter", "gantMainBouclier": "Shield hand glove", "gantMainsArmées": "Armed hand(s) glove(s)", @@ -597,68 +660,5 @@ "voirLesStatues": "View statues", "vousNêtesPasEncoreInscrit": "You are not yet registered or your registration has not yet been entered on the intranet", "à": "at", - "étatDeLaDemande": "Request status", - "fileImport.variants": { - "licence": [ - "license", - "licence", - "license number", - "license ID", - "ID license", - "licence no" - ], - "pays": [ - "country", - "pays", - "country of residence", - "origin country" - ], - "nom": [ - "last name", - "nom", - "family name", - "surname", - "lastname" - ], - "prenom": [ - "first name", - "prénom", - "given name", - "first given name" - ], - "genre": [ - "gender", - "genre", - "sex", - "civility" - ], - "weight": [ - "weight", - "poids", - "weight (kg)", - "actual weight", - "mass" - ], - "categorie": [ - "category", - "catégorie", - "weight category", - "age category" - ], - "overCategory": [ - "over category", - "surclassement", - "category override", - "over classification" - ], - "club": [ - "club", - "club name", - "association", - "association name" - ] - }, - "comp.toast.registers.addMultiple.error": "Import failed", - "comp.toast.registers.addMultiple.pending": "Import in progress", - "comp.toast.registers.addMultiple.success": "Import completed successfully 🎉" + "étatDeLaDemande": "Request status" } diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 94153d3..0e3ff49 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -5,13 +5,21 @@ "actuel": "Actuel", "administration": "Administration", "adresseDuServeur": "Adresse du serveur", + "advertisement": "Advertisement", "ajouter": "Ajouter", "ajouterDesCombattants": "Ajouter des combattants", + "ajouterUn": "Ajouter un ", "ajouterUneTeam": "Ajouter une équipe", "attention": "Attention", "aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une", + "avertissement": "Avertissement", "bleu": "Bleu", "blue": "Blue", + "cardAdded": "Carton ajouté", + "cardRemoved": "Carton retiré", + "cartonJaune": "Carton jaune", + "cartonNoir": "Carton noir", + "cartonRouge": "Carton rouge", "catégorie": "Catégorie", "chrono.+/-...S": "+/- ... s", "chrono.+10S": "+10 s", @@ -47,6 +55,7 @@ "duréePause": "Durée pause", "duréeRound": "Durée round", "editionDeLaCatégorie": "Edition de la catégorie", + "editionDuMatch": "Edition du match", "enregister": "Enregister", "enregistrer": "Enregistrer", "epéeBouclier": "Epée bouclier", @@ -100,6 +109,7 @@ "serveur": "Serveur", "suivant": "Suivant", "supprimer": "Supprimer", + "supprimerUn": "Supprimer un", "sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage", "sélectionner": "Sélectionner", "team": "Équipe", diff --git a/src/main/webapp/public/locales/fr/common.json b/src/main/webapp/public/locales/fr/common.json index 8a11036..790d076 100644 --- a/src/main/webapp/public/locales/fr/common.json +++ b/src/main/webapp/public/locales/fr/common.json @@ -247,6 +247,9 @@ "comp.toast.register.self.del.error": "Erreur lors de la désinscription", "comp.toast.register.self.del.pending": "Désinscription en cours", "comp.toast.register.self.del.success": "Désinscription réalisée", + "comp.toast.registers.addMultiple.error": "Erreur lors de l'importation des combattants", + "comp.toast.registers.addMultiple.pending": "Importation des combattants en cours...", + "comp.toast.registers.addMultiple.success": "Importation des combattants réussie 🎉", "comp.toast.save.error": "Échec de l'enregistrement de la compétition", "comp.toast.save.pending": "Enregistrement de la compétition en cours", "comp.toast.save.success": "Compétition enregistrée avec succès 🎉", @@ -309,6 +312,70 @@ "f": "F", "faitPar": "Fait par", "femme": "Femme", + "fileImport.variants": { + "categorie": [ + "catégorie", + "category", + "catégorie de poids", + "weight category", + "catégorie d'âge" + ], + "club": [ + "club", + "nom du club", + "club name", + "association", + "nom de l'association" + ], + "genre": [ + "genre", + "sexe", + "gender", + "sex", + "civilité" + ], + "licence": [ + "licence", + "n° licence", + "num licence", + "id licence", + "license", + "licence id" + ], + "nom": [ + "nom", + "nom de famille", + "lastname", + "family name", + "nom complet" + ], + "overCategory": [ + "surclassement", + "over category", + "surcatégorie", + "surclassement de catégorie" + ], + "pays": [ + "pays", + "country", + "pays de résidence", + "pays d'origine" + ], + "prenom": [ + "prénom", + "prenom", + "first name", + "given name", + "prénom usuel" + ], + "weight": [ + "poids", + "weight", + "poids (kg)", + "poids réel", + "masse" + ] + }, "filtre": "Filtre", "gantMainBouclier": "Gant main de bouclier", "gantMainsArmées": "Gant main(s) armée(s)", @@ -597,72 +664,5 @@ "voirLesStatues": "Voir les statues", "vousNêtesPasEncoreInscrit": "Vous n'êtes pas encore inscrit ou votre inscription n'a pas encore été rentrée sur l'intranet", "à": "à", - "étatDeLaDemande": "État de la demande", - "fileImport.variants": { - "licence": [ - "licence", - "n° licence", - "num licence", - "id licence", - "license", - "licence id" - ], - "pays": [ - "pays", - "country", - "pays de résidence", - "pays d'origine" - ], - "nom": [ - "nom", - "nom de famille", - "lastname", - "family name", - "nom complet" - ], - "prenom": [ - "prénom", - "prenom", - "first name", - "given name", - "prénom usuel" - ], - "genre": [ - "genre", - "sexe", - "gender", - "sex", - "civilité" - ], - "weight": [ - "poids", - "weight", - "poids (kg)", - "poids réel", - "masse" - ], - "categorie": [ - "catégorie", - "category", - "catégorie de poids", - "weight category", - "catégorie d'âge" - ], - "overCategory": [ - "surclassement", - "over category", - "surcatégorie", - "surclassement de catégorie" - ], - "club": [ - "club", - "nom du club", - "club name", - "association", - "nom de l'association" - ] - }, - "comp.toast.registers.addMultiple.error": "Erreur lors de l'importation des combattants", - "comp.toast.registers.addMultiple.pending": "Importation des combattants en cours...", - "comp.toast.registers.addMultiple.success": "Importation des combattants réussie 🎉" + "étatDeLaDemande": "État de la demande" } diff --git a/src/main/webapp/src/hooks/useCard.jsx b/src/main/webapp/src/hooks/useCard.jsx new file mode 100644 index 0000000..ce5596e --- /dev/null +++ b/src/main/webapp/src/hooks/useCard.jsx @@ -0,0 +1,135 @@ +import {createContext, useContext, useEffect, useReducer} from "react"; +import {useWS} from "./useWS.jsx"; + +const CardContext = createContext({}); +const CardDispatchContext = createContext(() => { +}); + +function compareCards(a, b) { + for (const keys of Object.keys(a)) { + if (a[keys] !== b[keys]) { + return false; + } + } + return true; +} + +const CARD_TYPE_ORDER = [ + 'BLUE', + 'YELLOW', + 'RED', + 'BLACK' +] + +export function compareCardOrder(a, b) { + if (!a || !b) return 0; + return CARD_TYPE_ORDER.indexOf(a.type) - CARD_TYPE_ORDER.indexOf(b.type); +} + +function reducer(state, action) { + switch (action.type) { + case 'SET_CARD': + if (state[action.payload.id] === undefined || !compareCards(action.payload, state[action.payload.id])) { + return { + ...state, + [action.payload.id]: action.payload + } + } + return state + case 'SET_ALL': + if (action.payload.some(e => state[e.id] === undefined || !compareCards(e, state[e.id]))) { + const newCombs = {}; + for (const o of action.payload) { + newCombs[o.id] = o; + } + + return { + ...state, + ...newCombs + } + } + 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 + } + return state + default: + return state + } +} + +function WSListener({dispatch}) { + const {dispatch: dispatchWS} = useWS() + + useEffect(() => { + const sendCard = ({data}) => { + dispatch({type: 'SET_CARD', payload: data}); + } + const rmCard = ({data}) => { + dispatch({type: 'REMOVE_CARD', payload: data}); + } + + dispatchWS({type: 'addListener', payload: {callback: sendCard, code: 'sendCard'}}) + dispatchWS({type: 'addListener', payload: {callback: rmCard, code: 'rmCard'}}) + return () => { + dispatchWS({type: 'removeListener', payload: sendCard}) + dispatchWS({type: 'removeListener', payload: rmCard}) + } + }, []); + + return <> +} + +export function CardsProvider({children}) { + const [cards, dispatch] = useReducer(reducer, {}) + + return + + {children} + + + +} + +export function useCards() { + const cards = useContext(CardContext); + return { + cards, + cards_v: Object.values(cards), + ...useCardsStatic(Object.values(cards)) + } +} + +export function useCardsStatic(cards_v) { + return { + getCardInMatch: (match) => { + return cards_v.filter(card => (card.comb === match.c1 || card.comb === match.c2) && card.match === match.id); + }, + getHeightCardForCombInMatch: (combId, match) => { + return cards_v.filter(card => card.comb === combId && (card.category === match.categorie || (card.match !== match.id && card.type !== "BLUE"))).sort(compareCardOrder).pop() + } + } +} + +export function useCardsDispatch() { + return useContext(CardDispatchContext); +} + +export function hasEffectCard(card, matchId, categoryId) { + switch (card.type) { + case 'BLUE': + return false; + case 'YELLOW': + return card.match === matchId; + case 'RED': + return card.match === matchId || card.category === categoryId; + case 'BLACK': + return true; + default: + return false; + } +} diff --git a/src/main/webapp/src/hooks/useComb.jsx b/src/main/webapp/src/hooks/useComb.jsx index 7ddf0a5..92a3936 100644 --- a/src/main/webapp/src/hooks/useComb.jsx +++ b/src/main/webapp/src/hooks/useComb.jsx @@ -78,7 +78,7 @@ function WSListener({dispatch}) { dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}}) return () => { - dispatchWS({type: 'removeListener', payload: {callback: sendRegister, code: 'sendRegister'}}) + dispatchWS({type: 'removeListener', payload: sendRegister}) } }, []); diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index d2f4a71..328cc77 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState, useReducer} from "react"; +import React, {useEffect, useReducer, useRef, useState} from "react"; import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; @@ -6,13 +6,12 @@ import {DrawGraph} from "../../result/DrawGraph.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {MarchReducer} from "../../../utils/MatchReducer.jsx"; -import {getToastMessage, scorePrint, win} from "../../../utils/Tools.js"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; -import {toast} from "react-toastify"; +import {virtualScore, win_end} from "../../../utils/Tools.js"; import "./CMTMatchPanel.css" import {useOBS} from "../../../hooks/useOBS.jsx"; import {useTranslation} from "react-i18next"; +import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; +import {ScorePanel} from "./ScoreAndCardPanel.jsx"; function CupImg() { return { setTrees(data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))) + cardDispatch({type: 'SET_ALL', payload: data.cards}); + let matches2 = []; let combsToAdd = []; data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd)); @@ -128,21 +130,15 @@ function CMTMatchPanel({catId, cat, menuActions}) { reducer({type: 'REMOVE', payload: data}) } - const sendCardboard = ({data}) => { - reducer({type: 'UPDATE_CARDBOARD', payload: {...data}}) - } - dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}}) dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}}) dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}}) dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}}) - dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}}) return () => { dispatch({type: 'removeListener', payload: treeListener}) dispatch({type: 'removeListener', payload: matchListener}) dispatch({type: 'removeListener', payload: matchOrder}) dispatch({type: 'removeListener', payload: deleteMatch}) - dispatch({type: 'removeListener', payload: sendCardboard}) } }, [catId]); @@ -195,11 +191,12 @@ function MatchList({matches, cat, menuActions}) { const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1") const publicAffDispatch = usePubAffDispatch(); const {t} = useTranslation("cm"); + const {cards, getHeightCardForCombInMatch} = useCards(); 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: win(m.scores)})) + .map(m => ({...m, ...win_end(m, Object.values(cards))})) const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; const isActiveMatch = (index) => { @@ -242,6 +239,33 @@ function MatchList({matches, cat, menuActions}) { setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id); }, [matches]) + + + const GetCard = ({combId, match, cat}) => { + const c = getHeightCardForCombInMatch(combId, match) + if (!c) + return <> + let bg = ""; + switch (c.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 + } + return <> {liceName.length > 1 &&
@@ -273,7 +297,7 @@ function MatchList({matches, cat, menuActions}) { {marches2.map((m, index) => ( setActiveMatch(m.id)}> {liceName[(index - firstIndex) % liceName.length]} @@ -282,9 +306,11 @@ function MatchList({matches, cat, menuActions}) { {index >= firstIndex ? index + 1 - firstIndex : ""} {m.end && m.win > 0 && } - + + - + + {m.end && m.win < 0 && } ))} @@ -302,6 +328,7 @@ function BuildTree({treeData, matches, menuActions}) { const [currentMatch, setCurrentMatch] = useState(null) const {getComb} = useCombs() const publicAffDispatch = usePubAffDispatch(); + const {cards_v} = useCards(); const match = matches.find(m => m.id === currentMatch?.matchSelect) useEffect(() => { @@ -328,9 +355,20 @@ function BuildTree({treeData, matches, menuActions}) { const c1 = getComb(matchData?.c1) const c2 = getComb(matchData?.c2) + const scores2 = [] + for (const score of matchData?.scores) { + scores2.push({ + ...score, + s1: virtualScore(matchData?.c1, score, matchData, cards_v), + s2: virtualScore(matchData?.c2, score, matchData, cards_v) + }) + } + let node = new TreeNode({ ...matchData, + ...win_end(matchData, cards_v), + scores: scores2, c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null, c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null }) @@ -362,7 +400,7 @@ function BuildTree({treeData, matches, menuActions}) {
+ matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23} cards={cards_v}/>
@@ -371,301 +409,3 @@ function BuildTree({treeData, matches, menuActions}) { menuActions={menuActions}/>}
} - -function ScorePanel({matchId, matchs, match, menuActions}) { - const onClickVoid = useRef(() => { - }); - - return
- - -
-} - -function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { - const {sendRequest} = useWS() - const setLoading = useLoadingSwitcher() - const {t} = useTranslation("cm"); - - const [end, setEnd] = useState(match?.end || false) - const [scoreIn, setScoreIn] = useState("") - const inputRef = useRef(null) - const tableRef = useRef(null) - const scoreRef = useRef([]) - const lastScoreClick = useRef(null) - const scoreInRef = useRef(null) - - useEffect(() => { - scoreInRef.current = scoreIn; - }, [scoreIn]); - - useEffect(() => { - menuActions.current.saveScore = (scoreRed, scoreBlue) => { - const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; - const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue}; - toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore", "cm")); - } - return () => menuActions.current.saveScore = undefined; - }, [matchId]) - - const handleScoreClick = (e, round, comb) => { - e.stopPropagation(); - const tableRect = tableRef.current.getBoundingClientRect(); - const rect = e.currentTarget.getBoundingClientRect(); - - updateScore(); - - const sel = inputRef.current; - sel.style.top = (rect.y - tableRect.y) + "px"; - sel.style.left = (rect.x - tableRect.x) + "px"; - sel.style.width = rect.width + "px"; - sel.style.height = rect.height + "px"; - sel.style.display = "block"; - - if (round === -1) { - const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; - setScoreIn(""); - console.log("Setting for new round", maxRound); - lastScoreClick.current = {matchId: matchId, round: maxRound, comb}; - } else { - const score = match.scores.find(s => s.n_round === round); - setScoreIn((comb === 1 ? score?.s1 : score?.s2) || ""); - lastScoreClick.current = {matchId: matchId, round, comb}; - setTimeout(() => inputRef.current.select(), 100); - } - } - - const updateScore = () => { - if (lastScoreClick.current !== null) { - const {matchId, round, comb} = lastScoreClick.current; - lastScoreClick.current = null; - - const scoreIn_ = String(scoreInRef.current).trim() === "" ? -1000 : Number(scoreInRef.current); - - const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round); - - let newScore; - if (score) { - if (comb === 1) - newScore = {...score, s1: scoreIn_}; - else - newScore = {...score, s2: scoreIn_}; - - if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2) - return - } else { - newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)}; - if (newScore.s1 === -1000 && newScore.s2 === -1000) - return - } - - setLoading(1) - sendRequest('updateMatchScore', {matchId: matchId, ...newScore}) - .finally(() => { - setLoading(0) - }) - } - } - - const onClickVoid = () => { - updateScore(); - - const sel = inputRef.current; - sel.style.display = "none"; - lastScoreClick.current = null; - } - onClickVoid_.current = onClickVoid; - - useEffect(() => { - if (!match || match?.end === end) - return; - - if (end) { - if (win(match?.scores) === 0 && match.categorie_ord === -42) { - toast.error(t('score.err1')); - setEnd(false); - return; - } - } - - setLoading(1) - sendRequest('updateMatchEnd', {matchId: matchId, end}) - .finally(() => { - setLoading(0) - }) - }, [end]); - - useEffect(() => { - onClickVoid() - }, [matchId]); - - useEffect(() => { - if (match?.scores) - scoreRef.current = scoreRef.current.slice(0, match.scores.length); - }, [match?.scores]); - - useEffect(() => { - if (!match) - return; - setEnd(match.end); - }, [match]); - - useEffect(() => { - const handleClickOutside = (event) => { - if (inputRef.current && !inputRef.current.contains(event.target)) { - onClickVoid(); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); - - const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') - const o = [...tooltipTriggerList] - o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) - - const tt = t('score.spe') - - const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0; - return
-
{t('scores')}
- - - - - - - - - - {match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => ( - - - - - - ))} - - - - - - -
{t('manche')}{t('rouge')}{t('bleu')}
{score.n_round + 1} scoreRef.current[score.n_round * 2] = e} - onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)} scoreRef.current[score.n_round * 2 + 1] = e} - onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}
scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>- scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>- -
-
-
- setEnd(e.target.checked)}/> - -
-
- setScoreIn(e.target.value)} - onClick={e => e.stopPropagation()} - onKeyDown={e => { - if (e.key === "Tab") { - if (lastScoreClick.current !== null) { - const {round, comb} = lastScoreClick.current; - const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1); - if (nextIndex >= 0 && nextIndex < scoreRef.current.length) { - e.preventDefault(); - scoreRef.current[nextIndex].click(); - } - } - } else if (e.key === "Enter") { - e.preventDefault(); - onClickVoid(); - } - }}/> -
-} - -function CardPanel({matchId, match}) { - const {sendRequest, dispatch} = useWS(); - const setLoading = useLoadingSwitcher() - - const {data, refresh} = useRequestWS('getCardboardWithoutThis', matchId, setLoading); - - useEffect(() => { - refresh('getCardboardWithoutThis', matchId); - - const sendCardboard = ({data}) => { - if (data.comb_id === match.c1 || data.comb_id === match.c2) { - refresh('getCardboardWithoutThis', matchId); - } - } - - dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}}) - return () => dispatch({type: 'removeListener', payload: sendCardboard}) - }, [matchId]) - - if (!match) { - return
- } - - const c1Cards = match.cardboard?.find(c => c.comb_id === match.c1) || {red: 0, yellow: 0}; - const c2Cards = match.cardboard?.find(c => c.comb_id === match.c2) || {red: 0, yellow: 0}; - - const handleCard = (combId, yellow, red) => { - if (combId === match.c1) { - if (c1Cards.red + red < 0 || c1Cards.yellow + yellow < 0) - return; - } else if (combId === match.c2) { - if (c2Cards.red + red < 0 || c2Cards.yellow + yellow < 0) - return; - } else { - return; - } - - setLoading(1) - sendRequest('sendCardboardChange', {matchId, combId, yellow, red}) - .finally(() => { - setLoading(0) - }) - } - - return
-
Carton
-
-
Competition: {(data?.c1_red || 0) + c1Cards.red} {(data?.c1_yellow || 0) + c1Cards.yellow}
-
- Match: -
- - {c1Cards.red} - -
-
- - {c1Cards.yellow} - -
-
-
-
-
Competition: {(data?.c2_red || 0) + c2Cards.red} {(data?.c2_yellow || 0) + c2Cards.yellow}
-
- Match: -
- - {c2Cards.red} - -
-
- - {c2Cards.yellow} - -
-
-
-
-} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index bb4ad89..6f67a51 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -6,7 +6,7 @@ import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; import {DrawGraph} from "../../result/DrawGraph.jsx"; import {SelectCombModalContent} from "./SelectCombModalContent.jsx"; -import {createMatch, scoreToString} from "../../../utils/CompetitionTools.js"; +import {createMatch, scoreToString2} from "../../../utils/CompetitionTools.js"; import {DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core'; import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable'; @@ -14,9 +14,12 @@ import {useSortable} from '@dnd-kit/sortable'; import {CSS} from '@dnd-kit/utilities'; import {toast} from "react-toastify"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faTrash} from "@fortawesome/free-solid-svg-icons"; -import {getToastMessage, win} from "../../../utils/Tools.js"; +import {faPen, faTrash} from "@fortawesome/free-solid-svg-icons"; +import {getToastMessage, virtualScore, win_end} from "../../../utils/Tools.js"; import {useTranslation} from "react-i18next"; +import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; + +import {ScorePanel} from "./ScoreAndCardPanel.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -29,6 +32,7 @@ function CupImg() { export function CategoryContent({cat, catId, setCat, menuActions}) { const setLoading = useLoadingSwitcher() const {sendRequest, dispatch} = useWS(); + const cardDispatch = useCardsDispatch(); const [matches, reducer] = useReducer(MarchReducer, []); const [groups, setGroups] = useState([]) const groupsRef = useRef(groups); @@ -121,6 +125,8 @@ export function CategoryContent({cat, catId, setCat, menuActions}) { trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)) }) + cardDispatch({type: 'SET_ALL', payload: data.cards}); + let matches2 = []; let combsToAdd = []; data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd)); @@ -463,12 +469,19 @@ function MatchList({matches, cat, groups, reducer}) { const [combSelect, setCombSelect] = useState(0) const [combC1nm, setCombC1nm] = useState(null) const [combC2nm, setCombC2nm] = useState(null) + const [modalMatchId, setModalMatchId] = useState(null) const {t} = useTranslation("cm"); + const {cards_v, getHeightCardForCombInMatch} = useCards(); + const matchModal = useRef(null); 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: win(m.scores)})) + .map(m => ({...m, ...win_end(m, cards_v)})) + marches2.forEach(m => { + if (m.end && (!m.scores || m.scores.length === 0)) + m.scores = [{n_round: 0, s1: 0, s2: 0}]; + }) const sensors = useSensors( useSensor(PointerSensor), @@ -568,6 +581,14 @@ function MatchList({matches, cat, groups, reducer}) { .finally(() => setLoading(0)) } + const handleEditMatch = (matchId) => { + const match = matches.find(m => m.id === matchId) + if (!match) + return; + setModalMatchId (matchId); + matchModal.current.click(); + } + const handleCombClick = (e, matchId, combId) => { e.stopPropagation(); const tableRect = tableRef.current.getBoundingClientRect(); @@ -590,6 +611,31 @@ function MatchList({matches, cat, groups, reducer}) { lastMatchClick.current = null; } + const GetCard = ({combId, match, cat}) => { + const c = getHeightCardForCombInMatch(combId, match) + if (!c) + return <> + let bg = ""; + switch (c.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 + } + const combsIDs = groups.map(m => m.id); return
@@ -608,6 +654,7 @@ function MatchList({matches, cat, groups, reducer}) { {t('résultat')} + @@ -617,14 +664,16 @@ function MatchList({matches, cat, groups, reducer}) { {m.poule} {liceName[index % liceName.length]} {m.end && m.win > 0 && } - handleCombClick(e, m.id, m.c1)}> - - handleCombClick(e, m.id, m.c2)}> - + + + + + + {m.end && m.win < 0 && } - {scoreToString(m.scores)} + {scoreToString2(m, cards_v)} + handleEditMatch(m.id)}> + handleDelMatch(m.id)}> ☰ @@ -659,9 +708,37 @@ function MatchList({matches, cat, groups, reducer}) { ))} + + +
} +function MatchEditModalContent({matchId, matches}) { + const menuActionsLocal = useRef({}); + const match = matches.find(m => m.id === matchId) + const {t} = useTranslation("cm"); + + return <> +
+

{t('editionDuMatch')}

+ +
+
+ +
+
+ +
+ +} + function SortableRow({id, children}) { const { attributes, @@ -701,6 +778,7 @@ function BuildTree({treeData, matches, groups}) { const {sendRequest} = useWS(); const setLoading = useLoadingSwitcher() const {t} = useTranslation("cm"); + const {cards_v} = useCards(); function parseTree(data_in) { if (data_in?.data == null) @@ -710,9 +788,19 @@ function BuildTree({treeData, matches, groups}) { const c1 = getComb(matchData?.c1) const c2 = getComb(matchData?.c2) + const scores2 = [] + for (const score of matchData?.scores) { + scores2.push({ + ...score, + s1: virtualScore(matchData?.c1, score, matchData, cards_v), + s2: virtualScore(matchData?.c2, score, matchData, cards_v) + }) + } let node = new TreeNode({ ...matchData, + ...win_end(matchData, cards_v), + scores: scores2, c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null, c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null }) @@ -789,7 +877,7 @@ function BuildTree({treeData, matches, groups}) { const combsIDs = groups.map(m => m.id); return
- + setEnd(e.target.checked)}/> + +
+ + setScoreIn(e.target.value)} + onClick={e => e.stopPropagation()} + onKeyDown={e => { + if (e.key === "Tab") { + if (lastScoreClick.current !== null) { + const {round, comb} = lastScoreClick.current; + const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1); + if (nextIndex >= 0 && nextIndex < scoreRef.current.length) { + e.preventDefault(); + scoreRef.current[nextIndex].click(); + } + } + } else if (e.key === "Enter") { + e.preventDefault(); + onClickVoid(); + } + }}/> + +} + +function CardPanel({matchId, match, vEnd, admin}) { + const {sendRequest} = useWS(); + const setLoading = useLoadingSwitcher() + const cardDispatch = useCardsDispatch(); + const {getCardInMatch, getHeightCardForCombInMatch} = useCards(); + const {t} = useTranslation("cm"); + + const {data, refresh} = useRequestWS('getCardForMatch', matchId, setLoading); + useEffect(() => { + refresh('getCardForMatch', matchId); + }, [matchId]) + + useEffect(() => { + if (!data) + return; + cardDispatch({type: 'SET_ALL', payload: data}) + }, [data]) + + if (!match) { + return
+ } + const handleCard = (combId, type) => { + setLoading(1) + sendRequest('sendCardAdd', {matchId, combId, type}) + .then(() => { + toast.success(t('cardAdded')); + }) + .catch(err => { + toast.error(err); + }) + .finally(() => { + setLoading(0) + }) + } + + const handleCardRm = (combId, type) => { + setLoading(1) + sendRequest('sendCardRm', {matchId, combId, type}) + .then(() => { + toast.success(t('cardRemoved')); + }) + .catch(err => { + toast.error(err); + }) + .finally(() => { + setLoading(0) + }) + } + + const cards = getCardInMatch(match) + const MakeList = ({comb}) => { + const card = getHeightCardForCombInMatch(comb, match); + + return
+ + + + +
+ } + + const MakeListAdmin = ({comb}) => { + const card = getHeightCardForCombInMatch(comb, match); + + return
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ } + + return
+
Carton
+
+
Combattant rouge
+ {admin ? : + <>{t('ajouterUn')}} +
+
+
Combattant bleu
+ {admin ? : + <>{t('ajouterUn')}} +
+
+} diff --git a/src/main/webapp/src/pages/result/DrawGraph.jsx b/src/main/webapp/src/pages/result/DrawGraph.jsx index c518ea7..dd490fa 100644 --- a/src/main/webapp/src/pages/result/DrawGraph.jsx +++ b/src/main/webapp/src/pages/result/DrawGraph.jsx @@ -1,5 +1,6 @@ import {useEffect, useRef} from "react"; -import {scorePrint, win} from "../../utils/Tools.js"; +import {scorePrint} from "../../utils/Tools.js"; +import {compareCardOrder, useCardsStatic} from "../../hooks/useCard.jsx"; const max_x = 500; @@ -20,12 +21,14 @@ export function DrawGraph({ }, matchSelect = null, matchNext = null, - size = 24 + size = 24, + cards = [] }) { const canvasRef = useRef(null); const actionCanvasRef = useRef(null); const ctxARef = useRef(null); const actionMapRef = useRef({}); + const {getHeightCardForCombInMatch} = useCardsStatic(cards); const selectColor = "#30cc30"; @@ -149,6 +152,40 @@ export function DrawGraph({ ctx.restore(); }; + const printCard = (ctx, pos, combId, match) => { + const cards2 = getHeightCardForCombInMatch(combId, match) + if (cards2 != null) { + let oldColor = ctx.fillStyle; + switch (cards2.type) { + case "BLUE": + ctx.fillStyle = "#2e2efd"; + break; + case "YELLOW": + ctx.fillStyle = "#d8d800"; + break; + case "RED": + ctx.fillStyle = "#FF0000"; + break; + case "BLACK": + ctx.fillStyle = "#000000"; + break; + default: + ctx.fillStyle = "#FFFFFF00"; + } + + if (cards2.match === match.id) { + ctx.beginPath(); + ctx.strokeStyle = ctx.fillStyle + ctx.arc(pos.x + pos.width - 10, pos.y + 5, 5, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + ctx.strokeStyle = "#000000" + } else + ctx.fillRect(pos.x + pos.width - 18, pos.y - 5, 12, 12); + ctx.fillStyle = oldColor; + } + } + const newColor = () => { const letters = '0123456789ABCDEF' let color @@ -182,7 +219,9 @@ export function DrawGraph({ printScores(ctx, match.scores, px, py, 1); + const pos = {x: px - size * 2 - size * 8, y: py - size - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)} + printCard(ctx, pos, match.c1, match) ctx.fillStyle = "#FF0000" printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true) ctxA.fillStyle = newColor() @@ -190,6 +229,7 @@ export function DrawGraph({ actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1} const pos2 = {x: px - size * 2 - size * 8, y: py + size - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)} + printCard(ctx, pos2, match.c2, match) ctx.fillStyle = "#0000FF" printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true) ctxA.fillStyle = newColor() @@ -228,6 +268,7 @@ export function DrawGraph({ printScores(ctx, match.scores, px, py, 1.5); const pos = {x: px - size * 2 - size * 8, y: py - size * 2 * death - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)} + printCard(ctx, pos, match.c1, match) ctx.fillStyle = "#FF0000" printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true) ctxA.fillStyle = newColor() @@ -235,6 +276,7 @@ export function DrawGraph({ actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1} const pos2 = {x: px - size * 2 - size * 8, y: py + size * 2 * death - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)} + printCard(ctx, pos2, match.c2, match) ctx.fillStyle = "#0000FF" printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true) ctxA.fillStyle = newColor() @@ -310,7 +352,7 @@ export function DrawGraph({ for (const node of root) { let win_name = ""; if (node.data.end) { - win_name = win(node.data.scores) > 0 + win_name = node.data.win > 0 ? (node.data.c1FullName === null ? "???" : node.data.c1FullName) : (node.data.c2FullName === null ? "???" : node.data.c2FullName); } diff --git a/src/main/webapp/src/utils/CompetitionTools.js b/src/main/webapp/src/utils/CompetitionTools.js index 81e14d2..95a0710 100644 --- a/src/main/webapp/src/utils/CompetitionTools.js +++ b/src/main/webapp/src/utils/CompetitionTools.js @@ -1,4 +1,4 @@ -import {scorePrint} from "./Tools.js"; +import {scorePrint, virtualScore} from "./Tools.js"; export function scoreToString(score) { if (score === null || score === undefined || score.length === 0) @@ -10,6 +10,18 @@ export function scoreToString(score) { return score.sort((a, b) => a.n_round - b.n_round).map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | ") } +export function scoreToString2(match, cards = []) { + if (!match || match.scores === null || match.scores === undefined || match.scores.length === 0) + return "" + + if (Array.isArray(match.scores[0])) + return match.scores.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ") + else { + return match.scores.sort((a, b) => a.n_round - b.n_round).map(o => + scorePrint(virtualScore(match.c1, o, match, cards)) + "-" + scorePrint(virtualScore(match.c2, o, match, cards))).join(" | ") + } +} + export function createMatch(category, matchs, groups) { const combs_2 = {} for (const group of groups) { diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index af3c5cf..1dfb27d 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -1,5 +1,6 @@ import axios from "axios"; import i18n from "../config/i18n.js"; +import {compareCardOrder, hasEffectCard} from "../hooks/useCard.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -225,9 +226,18 @@ export function getToastMessage(msgKey, ns = 'common') { } } -export function win(scores) { +export function win(match, cards = []) { + const cards2 = cards.filter(c => (c.comb === match.c1 || c.comb === match.c2) && hasEffectCard(c, match.id, match.categorie)).sort(compareCardOrder).pop() + + if (cards2) { + if (cards2.comb === match.c1) + return 1 + else + return -1 + } + let sum = 0 - for (const score of scores) { + for (const score of match?.scores) { if (score.s1 === -1000 || score.s2 === -1000) continue if (score.s1 > score.s2) sum++ else if (score.s1 < score.s2) sum-- @@ -235,7 +245,62 @@ export function win(scores) { return sum } +export function win_end(match, cards = []) { + const cards2 = cards?.filter(c => (c.comb === match.c1 || c.comb === match.c2) && hasEffectCard(c, match.id, match.categorie)).sort(compareCardOrder) + + if (cards2.length > 1) { + if (cards2[0].comb !== cards2[1].comb) + return {win: 0, end: true} + } + if (cards2.length > 0) { + if (cards2[0].comb === match.c1) + return {win: -1, end: true} + else + return {win: 1, end: true} + } + + + let sum = 0 + for (const score of match?.scores) { + if (score.s1 === -1000 || score.s2 === -1000) continue + if (score.s1 > score.s2) sum++ + else if (score.s1 < score.s2) sum-- + } + return {win: sum, end: match?.end} +} + +export function virtual_end(match, cards) { + if (!cards) + return false + console.log(cards.filter(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie))) + return cards.some(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie)) +} + +export function virtualScore(combId, score, match, cards) { + const cards2 = cards?.filter(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie)).sort(compareCardOrder) + + if (cards2.length > 1) { + if (cards2[0].comb !== cards2[1].comb) + return -997 + } + if (cards2.length > 0) { + if (cards2[0].comb === combId) + return -997 + else + return 10 + } + + if (combId === match?.c1) { + return score.s1 + } else if (combId === match?.c2) { + return score.s2 + } else { + return "" + } +} + export function scorePrint(s1) { + switch (s1) { case -997: return "disc." -- 2.49.0 From d749dea6f45b2667ba75389a21158f8c85f8c264 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 27 Jan 2026 22:11:49 +0100 Subject: [PATCH 2/8] feat: add return of table state --- .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 29 +++ .../fr/titionfire/ffsaf/ws/recv/RCard.java | 2 + .../fr/titionfire/ffsaf/ws/recv/RMatch.java | 4 + .../fr/titionfire/ffsaf/ws/recv/RState.java | 149 +++++++++++++ .../fr/titionfire/ffsaf/ws/send/SSState.java | 19 ++ src/main/webapp/public/locales/en/cm.json | 6 +- src/main/webapp/public/locales/fr/cm.json | 6 +- src/main/webapp/src/hooks/useWS.jsx | 9 +- .../src/pages/competition/editor/CMAdmin.jsx | 46 +++- .../competition/editor/CMTChronoPanel.jsx | 20 ++ .../competition/editor/CMTMatchPanel.jsx | 17 +- .../src/pages/competition/editor/CMTPoint.jsx | 16 +- .../src/pages/competition/editor/CMTable.jsx | 69 +++++- .../pages/competition/editor/StateWindow.jsx | 211 ++++++++++++++++++ src/main/webapp/src/utils/Tools.js | 1 - 15 files changed, 576 insertions(+), 28 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java create mode 100644 src/main/webapp/src/pages/competition/editor/StateWindow.jsx diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index 7978fbd..56d8f42 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -51,6 +51,9 @@ public class CompetitionWS { @Inject RTeam rTeam; + @Inject + RState rState; + @Inject SecurityCtx securityCtx; @@ -95,6 +98,7 @@ public class CompetitionWS { getWSReceiverMethods(RRegister.class, rRegister); getWSReceiverMethods(RCard.class, rCard); getWSReceiverMethods(RTeam.class, rTeam); + getWSReceiverMethods(RState.class, rState); executor = notifyExecutor; } @@ -141,6 +145,7 @@ public class CompetitionWS { LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size()); waitingResponse.remove(connection); + rState.removeConnection(connection); } private MessageOut makeReply(MessageIn message, Object data) { @@ -230,6 +235,30 @@ public class CompetitionWS { }); } + public static void sendNotifyState(WebSocketConnection connection, String code, Object data) { + String uuid = connection.pathParam("uuid"); + + List> queue = new ArrayList<>(); + queue.add(Uni.createFrom().voidItem()); // For avoid empty queue + + connection.getOpenConnections().forEach(c -> { + Boolean s = c.userData().get(UserData.TypedKey.forBoolean("needState")); + if (uuid.equals(c.pathParam("uuid")) && s != null && s) { + queue.add(c.sendText(new MessageOut(UUID.randomUUID(), code, MessageType.NOTIFY, data))); + } + }); + + Uni.join().all(queue) + .andCollectFailures() + .runSubscriptionOn(executor) + .subscribeAsCompletionStage() + .whenComplete((v, t) -> { + if (t != null) { + LOGGER.error("Error sending ws_out message", t); + } + }); + } + @OnError Uni error(WebSocketConnection connection, ForbiddenException t) { return connection.close(CloseReason.INTERNAL_SERVER_ERROR); 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 c061c71..657b6db 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java @@ -58,6 +58,8 @@ public class RCard { @WSReceiver(code = "getCardForMatch", permission = PermLevel.VIEW) public Uni> getCardForMatch(WebSocketConnection connection, Long matchId) { + if (matchId == null) + return Uni.createFrom().nullItem(); return getById(matchId, connection).chain(matchModel -> cardService.getForMatch(matchModel)); } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java index a902ebe..aa99262 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -51,6 +51,9 @@ public class RMatch { @Inject TradService trad; + @Inject + RState rState; + private Uni getById(long id, WebSocketConnection connection) { return matchRepository.findById(id) .invoke(Unchecked.consumer(o -> { @@ -195,6 +198,7 @@ public class RMatch { return Panache.withTransaction(() -> matchRepository.persist(mm)); }) .invoke(mm -> toSend.add(MatchEntity.fromModel(mm))) + .invoke(mm -> rState.setMatchEnd(connection, matchEnd)) .chain(mm -> updateEndAndTree(mm, toSend)) .invoke(__ -> SSMatch.sendMatch(connection, toSend)) .replaceWithVoid(); diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java new file mode 100644 index 0000000..047dd94 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java @@ -0,0 +1,149 @@ +package fr.titionfire.ffsaf.ws.recv; + +import fr.titionfire.ffsaf.ws.PermLevel; +import fr.titionfire.ffsaf.ws.send.SSState; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.websockets.next.UserData; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import lombok.Data; + +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +@ApplicationScoped +@RegisterForReflection +public class RState { + + private static final HashMap tableStates = new HashMap<>(); + + @WSReceiver(code = "subscribeToState", permission = PermLevel.VIEW) + public Uni> sendCurrentScore(WebSocketConnection connection, Boolean subscribe) { + connection.userData().put(UserData.TypedKey.forBoolean("needState"), subscribe); + + if (subscribe) { + String uuid = connection.pathParam("uuid"); + return Uni.createFrom().item(() -> + tableStates.values().stream().filter(s -> s.getCompetitionUuid().equals(uuid)).toList() + ); + } + return Uni.createFrom().nullItem(); + } + + @WSReceiver(code = "sendState", permission = PermLevel.TABLE) + public Uni sendState(WebSocketConnection connection, TableState tableState) { + tableState.setCompetitionUuid(connection.pathParam("uuid")); + + if (tableStates.containsKey(connection)) + tableState.setId(tableStates.get(connection).getId()); + if (tableState.getChronoState().isRunning() && tableState.getChronoState().state == 0) + tableState.setState(MatchState.IN_PROGRESS); + tableStates.put(connection, tableState); + SSState.sendStateFull(connection, tableState); + return Uni.createFrom().voidItem(); + } + + @WSReceiver(code = "sendSelectCategory", permission = PermLevel.TABLE) + public Uni sendSelectCategory(WebSocketConnection connection, Long catId) { + TableState tableState = tableStates.get(connection); + if (tableState != null) { + tableState.setSelectedCategory(catId); + tableState.setState(MatchState.NOT_STARTED); + SSState.sendStateFull(connection, tableState); + } + return Uni.createFrom().voidItem(); + } + + @WSReceiver(code = "sendSelectMatch", permission = PermLevel.TABLE) + public Uni sendSelectMatch(WebSocketConnection connection, Long matchId) { + TableState tableState = tableStates.get(connection); + if (tableState != null) { + tableState.setSelectedMatch(matchId); + tableState.setState(MatchState.NOT_STARTED); + SSState.sendStateFull(connection, tableState); + } + return Uni.createFrom().voidItem(); + } + + @WSReceiver(code = "sendCurentChrono", permission = PermLevel.TABLE) + public Uni sendCurentChrono(WebSocketConnection connection, ChronoState chronoState) { + TableState tableState = tableStates.get(connection); + if (tableState != null) { + tableState.setChronoState(chronoState); + if (chronoState.isRunning()) + tableState.setState(MatchState.IN_PROGRESS); + SSState.sendStateFull(connection, tableState); + } + return Uni.createFrom().voidItem(); + } + + @WSReceiver(code = "sendLicenceName", permission = PermLevel.TABLE) + public Uni sendCurrentScore(WebSocketConnection connection, String name) { + TableState tableState = tableStates.get(connection); + if (tableState != null) { + tableState.setLiceName(name); + SSState.sendStateFull(connection, tableState); + } + return Uni.createFrom().voidItem(); + } + + @WSReceiver(code = "sendCurrentScore", permission = PermLevel.TABLE) + public Uni sendCurrentScore(WebSocketConnection connection, ScoreState scoreState) { + TableState tableState = tableStates.get(connection); + if (tableState != null) { + tableState.setScoreState(scoreState); + SSState.sendStateFull(connection, tableState); + } + return Uni.createFrom().voidItem(); + } + + public void removeConnection(WebSocketConnection connection) { + if (tableStates.containsKey(connection)) { + SSState.sendRmStateFull(connection, tableStates.get(connection).getId()); + tableStates.remove(connection); + } + } + + public void setMatchEnd(WebSocketConnection connection, RMatch.MatchEnd matchEnd) { + if (tableStates.containsKey(connection)) { + TableState tableState = tableStates.get(connection); + if (matchEnd.end()) + tableState.setState(MatchState.ENDED); + else + tableState.setState(MatchState.IN_PROGRESS); + SSState.sendStateFull(connection, tableState); + } + } + + @RegisterForReflection + public record ChronoState(long time, long startTime, long configTime, long configPause, int state) { + public boolean isRunning() { + return startTime != 0 || state != 0; + } + } + + @RegisterForReflection + public record ScoreState(int scoreRouge, int scoreBleu) { + } + + @Data + @RegisterForReflection + public static class TableState { + UUID id = UUID.randomUUID(); + String competitionUuid; + Long selectedCategory; + Long selectedMatch; + ChronoState chronoState; + ScoreState scoreState; + String liceName = "???"; + MatchState state = MatchState.NOT_STARTED; + } + + public enum MatchState { + NOT_STARTED, + IN_PROGRESS, + ENDED + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java new file mode 100644 index 0000000..6c6c826 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java @@ -0,0 +1,19 @@ +package fr.titionfire.ffsaf.ws.send; + +import fr.titionfire.ffsaf.ws.CompetitionWS; +import fr.titionfire.ffsaf.ws.recv.RState; +import io.quarkus.websockets.next.WebSocketConnection; + +import java.util.UUID; + +public class SSState { + + public static void sendStateFull(WebSocketConnection connection, RState.TableState state) { + CompetitionWS.sendNotifyState(connection, "sendStateFull", state); + } + + public static void sendRmStateFull(WebSocketConnection connection, UUID id) { + CompetitionWS.sendNotifyState(connection, "rmStateFull", id); + } + +} diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index dbb547d..a805338 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -5,7 +5,6 @@ "actuel": "Current", "administration": "Administration", "adresseDuServeur": "Server address", - "advertisement": "", "ajouter": "Add", "ajouterDesCombattants": "Add fighters", "ajouterUn": "Add one", @@ -40,6 +39,7 @@ "config.obs.motDePasseDuServeur": "Server password", "config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition", "config.obs.ws": "ws://", + "configurationDuNomDeLaZone": "Zone name configuration", "configurationObs": "OBS Configuration", "confirm1": "This match already has results; are you sure you want to delete it?", "confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!", @@ -64,6 +64,7 @@ "err3": "At least one type (pool or tournament) must be selected.", "erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ", "erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ", + "etatDesTablesDeMarque": "State of marque tables", "exporter": "Export", "fermer": "Close", "finalesUniquement": "Finals only", @@ -80,6 +81,7 @@ "neRienConserver": "Keep nothing", "no": "No.", "nom": "Name", + "nomDeLaZone": "Area name", "nomDeLéquipe": "team name", "nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')", "nouvelle...": "New...", @@ -146,7 +148,7 @@ "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", "ttm.admin.scripte": "Copy integration script", "ttm.table.inverserLaPosition": "Reverse fighter positions on this screen", - "ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration", + "ttm.table.obs": "Short click: Load configuration and connect.", "ttm.table.pub_aff": "Open public display", "ttm.table.pub_score": "Show scores on public display", "type": "Type", diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 0e3ff49..a0f1e20 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -5,7 +5,6 @@ "actuel": "Actuel", "administration": "Administration", "adresseDuServeur": "Adresse du serveur", - "advertisement": "Advertisement", "ajouter": "Ajouter", "ajouterDesCombattants": "Ajouter des combattants", "ajouterUn": "Ajouter un ", @@ -40,6 +39,7 @@ "config.obs.motDePasseDuServeur": "Mot de passe du serveur", "config.obs.warn1": "/! Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en changer entre chaque compétition", "config.obs.ws": "ws://", + "configurationDuNomDeLaZone": "Configuration du nom de la zone", "configurationObs": "Configuration OBS", "confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?", "confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!", @@ -64,6 +64,7 @@ "err3": "Au moins un type (poule ou tournoi) doit être sélectionné.", "erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ", "erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ", + "etatDesTablesDeMarque": "Etat des tables de marque", "exporter": "Exporter", "fermer": "Fermer", "finalesUniquement": "Finales uniquement", @@ -80,6 +81,7 @@ "neRienConserver": "Ne rien conserver", "no": "N°", "nom": "Nom", + "nomDeLaZone": "Nom de la zone", "nomDeLéquipe": "Nom de l'équipe", "nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')", "nouvelle...": "Nouvelle...", @@ -146,7 +148,7 @@ "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", "ttm.admin.scripte": "Copier le scripte d'intégration", "ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran", - "ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice", + "ttm.table.obs": "Clique court : Charger la configuration et se connecter.", "ttm.table.pub_aff": "Ouvrir l'affichage public", "ttm.table.pub_score": "Afficher les scores sur l'affichage public", "type": "Type", diff --git a/src/main/webapp/src/hooks/useWS.jsx b/src/main/webapp/src/hooks/useWS.jsx index 79fce96..3fdeada 100644 --- a/src/main/webapp/src/hooks/useWS.jsx +++ b/src/main/webapp/src/hooks/useWS.jsx @@ -47,6 +47,7 @@ export function WSProvider({url, onmessage, children}) { const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false}) const [state, dispatch] = useReducer(reducer, {listener: []}) const ws = useRef(null) + const tableState = useRef({}) const listenersRef = useRef([]) const callbackRef = useRef({}) const isReadyRef = useRef(isReady) @@ -216,14 +217,14 @@ export function WSProvider({url, onmessage, children}) { } - const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData} + const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData, tableState} return {children} } export function useWS() { - const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext) + const {isReady, dispatch, send, wait_length, welcomeData, tableState} = useContext(WebsocketContext) return { dispatch, isReady, @@ -247,6 +248,10 @@ export function useWS() { send(uuidv4(), "error", "ERROR", data) }, send, + setState: (newState) => { + tableState.current = {...tableState.current, ...newState} + }, + tableState } } diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index a2e301b..92ecf3c 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -11,10 +11,12 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import JSZip from "jszip"; import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx"; -import {faGlobe} from "@fortawesome/free-solid-svg-icons"; +import {faGlobe, faTableCellsLarge} 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"; const vite_url = import.meta.env.VITE_URL; @@ -161,12 +163,18 @@ async function downloadResourcesAsZip(resourceList) { progressText.textContent = i18n.t('téléchargementTerminé!'); } +const windowName = "FFSAFTableStateWindow"; + function Menu({menuActions, compUuid}) { const e = document.getElementById("actionMenu") const longPress = useRef({time: null, timer: null, button: null}); const obsModal = useRef(null); const {t} = useTranslation("cm"); + const [showStateWin, setShowStateWin] = useState(false) + const externalWindow = useRef(null) + const containerEl = useRef(document.createElement("div")) + for (const x of tto) x.dispose(); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]') @@ -178,6 +186,32 @@ function Menu({menuActions, compUuid}) { } } + useEffect(() => { + if (sessionStorage.getItem(windowName + "_open") === "true") { + handleStateWin(); + } + }, []); + + const handleStateWin = __ => { + if (showStateWin === false || !externalWindow.current || externalWindow.current.closed) { + externalWindow.current = window.open("", windowName, "width=800,height=600,left=200,top=200") + externalWindow.current.document.body.innerHTML = "" + externalWindow.current.document.body.appendChild(containerEl.current) + copyStyles(document, externalWindow.current.document) + + externalWindow.current.addEventListener("beforeunload", () => { + setShowStateWin(false); + externalWindow.current.close(); + externalWindow.current = null; + sessionStorage.removeItem(windowName + "_open"); + }); + setShowStateWin(true); + sessionStorage.setItem(windowName + "_open", "true"); + } else { + externalWindow.current.focus(); + } + } + const longPressDown = (button) => { longPress.current.button = button; longPress.current.time = new Date(); @@ -251,7 +285,12 @@ function Menu({menuActions, compUuid}) { onClick={() => copyScriptToClipboard()} data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.admin.scripte')}/> + , document.getElementById("actionMenu"))} + {externalWindow.current && createPortal(, containerEl.current)} @@ -181,5 +187,19 @@ export function ChronoPanel() { + + } + +function SendChrono({chrono, config, chronoState}) { + const {sendNotify, setState} = useWS(); + + useEffect(() => { + setState({chronoState: {...chrono, configTime: config.time, configPause: config.pause, state: chronoState}}); + sendNotify("sendCurentChrono", {...chrono, configTime: config.time, configPause: config.pause, state: chronoState}); + }, [chrono]); + + return <> + +} diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 328cc77..1f1055e 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -191,12 +191,13 @@ function MatchList({matches, cat, menuActions}) { const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1") const publicAffDispatch = usePubAffDispatch(); const {t} = useTranslation("cm"); - const {cards, getHeightCardForCombInMatch} = useCards(); + const {cards_v, getHeightCardForCombInMatch} = useCards(); + const {sendNotify, setState} = useWS(); 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, Object.values(cards))})) + .map(m => ({...m, ...win_end(m, cards_v), end: m.end})) const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; const isActiveMatch = (index) => { @@ -221,10 +222,6 @@ function MatchList({matches, cat, menuActions}) { }); } }, [match]); - //useEffect(() => { - // if (activeMatch !== null) - // setActiveMatch(null); - //}, [cat]) useEffect(() => { if (match && match.poule !== lice) @@ -240,6 +237,11 @@ function MatchList({matches, cat, menuActions}) { setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id); }, [matches]) + useEffect(() => { + setState({selectedMatch: activeMatch}); + sendNotify("sendSelectMatch", activeMatch); + }, [activeMatch]); + const GetCard = ({combId, match, cat}) => { const c = getHeightCardForCombInMatch(combId, match) @@ -329,6 +331,7 @@ function BuildTree({treeData, matches, menuActions}) { const {getComb} = useCombs() const publicAffDispatch = usePubAffDispatch(); const {cards_v} = useCards(); + const {sendNotify, setState} = useWS(); const match = matches.find(m => m.id === currentMatch?.matchSelect) useEffect(() => { @@ -390,6 +393,8 @@ function BuildTree({treeData, matches, menuActions}) { const onMatchClick = (rect, matchId, __) => { setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())}); + setState({selectedMatch: matchId}); + sendNotify("sendSelectMatch", matchId); } const onClickVoid = () => { diff --git a/src/main/webapp/src/pages/competition/editor/CMTPoint.jsx b/src/main/webapp/src/pages/competition/editor/CMTPoint.jsx index 8dbb05d..18a1752 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTPoint.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTPoint.jsx @@ -1,8 +1,9 @@ -import {useEffect, useState} from "react"; +import React, {useEffect, useState} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {useTranslation} from "react-i18next"; +import {useWS} from "../../../hooks/useWS.jsx"; export function PointPanel({menuActions}) { const [revers, setRevers] = useState(false) @@ -49,5 +50,18 @@ export function PointPanel({menuActions}) { + } + +function SendScore({scoreRouge, scoreBleu}) { + const {sendNotify, setState} = useWS(); + + useEffect(() => { + setState({scoreState: {scoreRouge, scoreBleu}}); + sendNotify("sendCurrentScore", {scoreRouge, scoreBleu}); + }, [scoreRouge, scoreBleu]); + + return <> + +} diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 9e2dc71..63bd969 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -59,6 +59,7 @@ export function CMTable() { + @@ -73,6 +74,7 @@ function Menu({menuActions}) { const publicAffDispatch = usePubAffDispatch() const [showPubAff, setShowPubAff] = useState(false) const [showScore, setShowScore] = useState(true) + const [zone, setZone] = useState(sessionStorage.getItem("liceName") || "???") const {connected, connect, disconnect} = useOBS(); const longPress = useRef({time: null, timer: null, button: null}); const obsModal = useRef(null); @@ -124,7 +126,7 @@ function Menu({menuActions}) { const longTimeAction = (button) => { if (button === "obs") { - obsModal.current.click(); + // obsModal.current.click(); } } @@ -169,12 +171,19 @@ function Menu({menuActions}) { } } - const handleOBSSubmit = (e) => { + const handleLiceSubmit = (e) => { e.preventDefault(); const form = e.target; const prefix = form[0].value; - sessionStorage.setItem("obs_prefix", prefix); + if (prefix === "") { + sessionStorage.removeItem("liceName"); + setZone("???"); + return; + } + + sessionStorage.setItem("liceName", prefix); + setZone(prefix); } if (!e) @@ -182,6 +191,8 @@ function Menu({menuActions}) { return <> {createPortal( <> +
+ obsModal.current.click()} style={{cursor: "pointer"}}>Zone {zone}
, document.getElementById("actionMenu"))} {externalWindow.current && createPortal(, containerEl.current)} - - 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); -- 2.49.0 From 4c260b86b9c15e3aaa11dab9e7886bac6a9008ba Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 29 Jan 2026 22:43:59 +0100 Subject: [PATCH 4/8] feat: remove old card system --- .../ffsaf/data/model/CardboardModel.java | 42 ------------------- .../ffsaf/data/model/MatchModel.java | 4 -- .../data/repository/CardboardRepository.java | 9 ---- .../ffsaf/domain/entity/CardboardEntity.java | 26 ------------ .../ffsaf/domain/entity/MatchEntity.java | 20 +-------- .../titionfire/ffsaf/ws/recv/RCategorie.java | 4 -- .../fr/titionfire/ffsaf/ws/recv/RMatch.java | 7 +--- .../fr/titionfire/ffsaf/ws/recv/RState.java | 14 ------- 8 files changed, 2 insertions(+), 124 deletions(-) delete mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java delete mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java delete mode 100644 src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java deleted file mode 100644 index 255f83e..0000000 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java +++ /dev/null @@ -1,42 +0,0 @@ -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; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@RegisterForReflection - -@Entity -@Table(name = "cardboard") -public class CardboardModel { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "comb", referencedColumnName = "id") - MembreModel comb; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "guest_comb", referencedColumnName = "id") - CompetitionGuestModel guestComb; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "match", referencedColumnName = "id") - MatchModel match; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "compet", referencedColumnName = "id") - CompetitionModel compet; - - int red; - int yellow; -} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java index e017810..c5a6c2f 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -63,10 +63,6 @@ public class MatchModel { char poule = 'A'; - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) - @JoinColumn(name = "match", referencedColumnName = "id") - List cardboard = new ArrayList<>(); - public String getC1Name(MembreModel model, ResultPrivacy privacy) { if (c1_id != null) return c1_id.getName(model, privacy); diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java deleted file mode 100644 index 11fc320..0000000 --- a/src/main/java/fr/titionfire/ffsaf/data/repository/CardboardRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package fr.titionfire.ffsaf.data.repository; - -import fr.titionfire.ffsaf.data.model.CardboardModel; -import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; -import jakarta.enterprise.context.ApplicationScoped; - -@ApplicationScoped -public class CardboardRepository implements PanacheRepositoryBase { -} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java deleted file mode 100644 index ca15a38..0000000 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/CardboardEntity.java +++ /dev/null @@ -1,26 +0,0 @@ -package fr.titionfire.ffsaf.domain.entity; - -import fr.titionfire.ffsaf.data.model.CardboardModel; -import io.quarkus.runtime.annotations.RegisterForReflection; -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -@RegisterForReflection -public class CardboardEntity { - long comb_id; - long match_id; - long compet_id; - - int red; - int yellow; - - public static CardboardEntity fromModel(CardboardModel model) { - return new CardboardEntity( - model.getComb() != null ? model.getComb().getId() : model.getGuestComb().getId() * -1, - model.getMatch().getId(), - model.getCompet().getId(), - model.getRed(), model.getYellow()); - } -} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchEntity.java index 49b7e77..cde3669 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchEntity.java @@ -6,7 +6,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; -import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -23,7 +22,6 @@ public class MatchEntity { private Date date; private List scores; private char poule; - private List cardboard; public static MatchEntity fromModel(MatchModel model) { if (model == null) @@ -35,22 +33,6 @@ public class MatchEntity { model.getC2_id()), model.getCategory_ord(), model.isEnd(), model.getCategory().getId(), model.getDate(), model.getScores(), - model.getPoule(), - (model.getCardboard() == null) ? new ArrayList<>() : model.getCardboard().stream() - .map(CardboardEntity::fromModel).toList()); - } - - public int win() { - int sum = 0; - for (ScoreEmbeddable score : scores) { - if (score.getS1() == -1000 || score.getS2() == -1000) - continue; - - if (score.getS1() > score.getS2()) - sum++; - else if (score.getS1() < score.getS2()) - sum--; - } - return sum; + model.getPoule()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index c6088b9..568373d 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -46,9 +46,6 @@ public class RCategorie { @Inject TreeRepository treeRepository; - @Inject - CardboardRepository cardboardRepository; - @Inject CardService cardService; @@ -221,7 +218,6 @@ public class RCategorie { public Uni deleteCategory(WebSocketConnection connection, Long id) { return getById(id, connection) .call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId()) - .call(__ -> cardboardRepository.delete("match.category = ?1", cat)) .call(__ -> matchRepository.delete("category = ?1", cat)))) .chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat))) .invoke(__ -> SSCategorie.sendDelCategory(connection, id)) diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java index aa99262..3490f56 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -45,9 +45,6 @@ public class RMatch { @Inject CompetitionGuestRepository competitionGuestRepository; - @Inject - CardboardRepository cardboardRepository; - @Inject TradService trad; @@ -289,9 +286,7 @@ public class RMatch { public Uni deleteMatch(WebSocketConnection connection, Long idMatch) { return getById(idMatch, connection) .map(__ -> idMatch) - .chain(l -> Panache.withTransaction(() -> - cardboardRepository.delete("match.id = ?1", l) - .chain(__ -> matchRepository.delete("id = ?1", l)))) + .chain(l -> Panache.withTransaction(() -> matchRepository.delete("id = ?1", l))) .invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch)) .replaceWithVoid(); } 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 88c3ffb..047dd94 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java @@ -117,20 +117,6 @@ 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() { -- 2.49.0 From 541d3824f3ae9567d3eda15ec69f4b4a986ac85b Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 30 Jan 2026 11:58:31 +0100 Subject: [PATCH 5/8] feat: update CardModel --- .../titionfire/ffsaf/data/model/CardModel.java | 11 ++++++++++- .../ffsaf/domain/entity/MatchModelExtend.java | 17 +++++++++++++++++ .../ffsaf/domain/service/CardService.java | 13 +++++++------ .../java/fr/titionfire/ffsaf/ws/recv/RCard.java | 3 ++- .../fr/titionfire/ffsaf/ws/recv/RCategorie.java | 2 +- 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java 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 f3b0c2c..1cb830f 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java @@ -1,5 +1,7 @@ package fr.titionfire.ffsaf.data.model; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.AllArgsConstructor; @@ -27,7 +29,14 @@ public class CardModel { Long comb; Long match; Long category; - Long competition; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "competition", referencedColumnName = "id") + CompetitionModel competition; + + @JsonProperty("competition") + Long competitionId; CardType type; String reason; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java new file mode 100644 index 0000000..ed0cdef --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java @@ -0,0 +1,17 @@ +package fr.titionfire.ffsaf.domain.entity; + +import fr.titionfire.ffsaf.data.model.CardModel; +import fr.titionfire.ffsaf.data.model.MatchModel; +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.util.List; + +@RegisterForReflection +public class MatchModelExtend{ + final MatchModel match; + + public MatchModelExtend(MatchModel match, List c) { + this.match = match; + } + +} 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 e96fd80..56c330f 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CardService.java @@ -62,18 +62,18 @@ public class CardService { 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", - match.getCategory().getCompet().getId(), COMPETITION_LEVEL_CARDS, + match.getCategory().getCompet(), COMPETITION_LEVEL_CARDS, extractCombIds(match), match.getCategory().getId()); } - public Uni> getAll(Long competitionId) { - return cardRepository.list("competition = ?1", competitionId); + public Uni> getAll(CompetitionModel competition) { + return cardRepository.list("competition = ?1", competition); } public Uni checkCanBeAdded(RCard.SendCardAdd card, MatchModel matchModel) { return cardRepository.find("competition = ?1 AND comb = ?2", Sort.descending("type"), - matchModel.getCategory().getCompet().getId(), card.combId()) + matchModel.getCategory().getCompet(), card.combId()) .firstResult() .map(card_ -> { if (card.type() == CardModel.CardType.BLUE) { @@ -119,7 +119,7 @@ public class CardService { .map(l -> l.stream().map(r -> r.getId() * -1).toList()); } }) - .chain(combIds -> cardRepository.list("competition = ?1 AND comb IN ?2", competition.getId(), combIds) + .chain(combIds -> cardRepository.list("competition = ?1 AND comb IN ?2", competition, combIds) .map(cards -> { List newCards = new ArrayList<>(); for (Long id : combIds) { @@ -127,7 +127,8 @@ public class CardService { .filter(c -> id.equals(c.getComb()) && c.getType() == type).findAny(); CardModel model = new CardModel(); - model.setCompetition(competition.getId()); + model.setCompetition(competition); + model.setCompetitionId(competition.getId()); model.setComb(id); model.setTeamCard(true); 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 f7b96fa..9c82819 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java @@ -83,7 +83,8 @@ public class RCard { model.setComb(card.combId()); model.setMatch(card.matchId()); model.setCategory(matchModel.getCategory().getId()); - model.setCompetition(matchModel.getCategory().getCompet().getId()); + model.setCompetition(matchModel.getCategory().getCompet()); + model.setCompetitionId(matchModel.getCategory().getCompet().getId()); model.setType(card.type()); model.setReason(card.reason()); diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index 568373d..dc977a4 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -86,7 +86,7 @@ public class RCategorie { .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId()) .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) .invoke(fullCategory::setTrees)) - .call(cat -> cardService.getAll(cat.getCompet().getId()) + .call(cat -> cardService.getAll(cat.getCompet()) .invoke(fullCategory::setCards)) .map(__ -> fullCategory); } -- 2.49.0 From 952300d063a7902725ce762c0e385dd61772e1be Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 30 Jan 2026 14:50:21 +0100 Subject: [PATCH 6/8] feat: card in result view --- .../ffsaf/data/model/CardModel.java | 17 +- .../ffsaf/domain/entity/MatchModelExtend.java | 169 +++++++++++++++++- .../ffsaf/domain/service/ResultService.java | 158 +++++++++------- .../ffsaf/rest/data/ResultCategoryData.java | 18 +- src/main/webapp/public/competition.js | 47 ++--- 5 files changed, 293 insertions(+), 116 deletions(-) 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 1cb830f..10c0b2c 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java @@ -4,16 +4,15 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hibernate.annotations.CreationTimestamp; import java.util.Date; +import java.util.Objects; @Getter @Setter +@ToString @AllArgsConstructor @NoArgsConstructor @RegisterForReflection @@ -46,6 +45,16 @@ public class CardModel { @Column(nullable = false, columnDefinition = "boolean default false") boolean teamCard = false; + public boolean hasEffect(MatchModel match) { + return switch (this.type) { + case BLUE -> false; + case YELLOW -> Objects.equals(this.match, match.getId()); + case RED -> Objects.equals(this.category, match.getCategory().getId()) + || Objects.equals(this.match, match.getId()); + case BLACK -> true; + }; + } + public enum CardType { BLUE, YELLOW, diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java index ed0cdef..f46dff4 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/MatchModelExtend.java @@ -1,17 +1,176 @@ package fr.titionfire.ffsaf.domain.entity; -import fr.titionfire.ffsaf.data.model.CardModel; -import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.data.model.*; +import fr.titionfire.ffsaf.utils.ResultPrivacy; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Getter; -import java.util.List; +import java.util.*; @RegisterForReflection -public class MatchModelExtend{ +public class MatchModelExtend { final MatchModel match; - public MatchModelExtend(MatchModel match, List c) { + @Getter + boolean isEnd = false; + @Getter + List scoresToPrint = new ArrayList<>(); + @Getter + List scoresToCompute = new ArrayList<>(); + @Getter + int win = 0; + + + public MatchModelExtend(MatchModel match, List cards) { this.match = match; + + List combIds = extractCombIds(match); + List cards2 = cards.stream().filter(c -> combIds.contains(c.getComb()) && c.hasEffect(match)) + .sorted(Comparator.comparing(CardModel::getType).reversed()).toList(); + + + for (ScoreEmbeddable score : match.getScores()) { + if (score.getS1() == -1000 || score.getS2() == -1000) + continue; + this.scoresToCompute.add(virtualScore(score, cards2, false)); + } + + calc_win_end(cards2); + + for (ScoreEmbeddable score : match.getScores()) { + if (score.getS1() == -1000 || score.getS2() == -1000) + continue; + this.scoresToPrint.add(virtualScore(score, cards2, true)); + } + if (this.isEnd && this.scoresToPrint.isEmpty()) { + this.scoresToPrint.add(virtualScore(new ScoreEmbeddable(0, 0, 0), cards2, true)); + } } + private ScoreEmbeddable virtualScore(ScoreEmbeddable score, List cards2, boolean toPrint) { + if (cards2.size() > 1) { + if (!Objects.equals(cards2.get(0).getComb(), cards2.get(1).getComb())) + return new ScoreEmbeddable(score.getN_round(), toPrint ? -997 : 0, toPrint ? -997 : 0); + } + if (!cards2.isEmpty()) { + if (isC1(cards2.get(0).getComb())) + return new ScoreEmbeddable(score.getN_round(), toPrint ? -997 : 0, 10); + else + return new ScoreEmbeddable(score.getN_round(), 10, toPrint ? -997 : 0); + } + + if (score.getS1() < -900 && score.getS2() < -900) + return new ScoreEmbeddable(score.getN_round(), toPrint ? score.getS1() : 0, toPrint ? score.getS2() : 0); + else if (score.getS1() < -900) + return new ScoreEmbeddable(score.getN_round(), toPrint ? score.getS1() : 0, 10); + else if (score.getS2() < -900) + return new ScoreEmbeddable(score.getN_round(), 10, toPrint ? score.getS2() : 0); + + return new ScoreEmbeddable(score.getN_round(), score.getS1(), score.getS2()); + } + + private void calc_win_end(List cards2) { + if (cards2.size() > 1) { + if (!Objects.equals(cards2.get(0).getComb(), cards2.get(1).getComb())) { + this.win = 0; + this.isEnd = true; + return; + } + } + + if (!cards2.isEmpty()) { + if (match.isC1(cards2.get(0).getComb())) { + this.win = -1; + } else if (match.isC2(cards2.get(0).getComb())) { + this.win = 1; + } + this.isEnd = true; + return; + } + + for (ScoreEmbeddable score : this.scoresToCompute) { + if (score.getS1() > score.getS2()) + win++; + else if (score.getS1() < score.getS2()) + win--; + } + this.isEnd = match.isEnd(); + } + + + private List extractCombIds(MatchModel match) { + List ids = new ArrayList<>(); + if (match.getC1_id() != null) + ids.add(match.getC1_id().getId()); + if (match.getC2_id() != null) + ids.add(match.getC2_id().getId()); + if (match.getC1_guest() != null) + ids.add(match.getC1_guest().getId() * -1); + if (match.getC2_guest() != null) + ids.add(match.getC2_guest().getId() * -1); + return ids; + } + + //--------------- Delegation methods to MatchModel --------------- + + public Long getId() { + return match.getId(); + } + + public MembreModel getC1_id() { + return match.getC1_id(); + } + + public CompetitionGuestModel getC1_guest() { + return match.getC1_guest(); + } + + public MembreModel getC2_id() { + return match.getC2_id(); + } + + public CompetitionGuestModel getC2_guest() { + return match.getC2_guest(); + } + + public CategoryModel getCategory() { + return match.getCategory(); + } + + public long getCategory_ord() { + return match.getCategory_ord(); + } + + public Date getDate() { + return match.getDate(); + } + + public char getPoule() { + return match.getPoule(); + } + + public String getC1Name(MembreModel model, ResultPrivacy privacy) { + return match.getC1Name(model, privacy); + } + + public String getC2Name(MembreModel model, ResultPrivacy privacy) { + return match.getC2Name(model, privacy); + } + + public String getC2Name() { + return match.getC2Name(); + } + + public String getC1Name() { + return match.getC1Name(); + } + + public boolean isC1(Object comb) { + return match.isC1(comb); + } + + public boolean isC2(Object comb) { + return match.isC2(comb); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index 024fe66..710f7c3 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.repository.*; +import fr.titionfire.ffsaf.domain.entity.MatchModelExtend; import fr.titionfire.ffsaf.rest.data.ResultCategoryData; import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; @@ -45,6 +46,9 @@ public class ResultService { @Inject MatchRepository matchRepository; + @Inject + CardRepository cardRepository; + @Inject TradService trad; @@ -123,39 +127,42 @@ public class ResultService { } public Uni getCategory(String uuid, long poule, SecurityCtx securityCtx) { - return hasAccess(uuid, securityCtx).chain(membreModel -> - matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule) - .call(list -> list.isEmpty() ? Uni.createFrom().voidItem() : - Mutiny.fetch(list.get(0).getCategory().getTree())) - .map(list -> getData(list, membreModel))); + return hasAccess(uuid, securityCtx).chain(membreModel -> getData(uuid, poule, membreModel)); } public Uni getCategory(String uuid, long poule) { + return getData(uuid, poule, null); + } + + private Uni getData(String uuid, long poule, MembreModel membreModel) { + List cards = new ArrayList<>(); + return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule) .call(list -> list.isEmpty() ? Uni.createFrom().voidItem() : Mutiny.fetch(list.get(0).getCategory().getTree())) - .map(list -> getData(list, null)); + .chain(list -> cardRepository.list("competition.uuid = ?1", uuid).invoke(cards::addAll) + .map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList())) + .map(matchModels -> { + ResultCategoryData out = new ResultCategoryData(); + + CategoryModel categoryModel = matchModels.get(0).getCategory(); + out.setName(categoryModel.getName()); + out.setType(categoryModel.getType()); + out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName() + .split(";")); + out.setGenTime(System.currentTimeMillis()); + + getArray2(matchModels, membreModel, out); + getTree(categoryModel.getTree(), membreModel, cards, out); + return out; + }); } - private ResultCategoryData getData(List matchModels, MembreModel membreModel) { - ResultCategoryData out = new ResultCategoryData(); + private void getArray2(List matchModels_, MembreModel membreModel, ResultCategoryData out) { + List matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList(); - CategoryModel categoryModel = matchModels.get(0).getCategory(); - out.setName(categoryModel.getName()); - out.setType(categoryModel.getType()); - out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";")); - out.setGenTime(System.currentTimeMillis()); - - getArray2(matchModels, membreModel, out); - getTree(categoryModel.getTree(), membreModel, out); - return out; - } - - private void getArray2(List matchModels_, MembreModel membreModel, ResultCategoryData out) { - List matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList(); - - HashMap> matchMap = new HashMap<>(); - for (MatchModel model : matchModels) { + HashMap> matchMap = new HashMap<>(); + for (MatchModelExtend model : matchModels) { char g = model.getPoule(); if (!matchMap.containsKey(g)) matchMap.put(g, new ArrayList<>()); @@ -164,7 +171,7 @@ public class ResultService { matchMap.forEach((c, matchEntities) -> { List matchs = matchEntities.stream() - .sorted(Comparator.comparing(MatchModel::getCategory_ord)) + .sorted(Comparator.comparing(MatchModelExtend::getCategory_ord)) .map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)) .toList(); @@ -204,26 +211,28 @@ public class ResultService { } private static void convertTree(TreeModel src, TreeNode dst, MembreModel membreModel, - ResultPrivacy privacy) { - dst.setData(ResultCategoryData.TreeData.from(src.getMatch(), membreModel, privacy)); + ResultPrivacy privacy, List cards) { + dst.setData( + ResultCategoryData.TreeData.from(new MatchModelExtend(src.getMatch(), cards), membreModel, privacy)); if (src.getLeft() != null) { dst.setLeft(new TreeNode<>()); - convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy); + convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy, cards); } if (src.getRight() != null) { dst.setRight(new TreeNode<>()); - convertTree(src.getRight(), dst.getRight(), membreModel, privacy); + convertTree(src.getRight(), dst.getRight(), membreModel, privacy, cards); } } - private void getTree(List treeModels, MembreModel membreModel, ResultCategoryData out) { + private void getTree(List treeModels, MembreModel membreModel, List cards, + ResultCategoryData out) { ArrayList> trees = new ArrayList<>(); treeModels.stream() .filter(t -> t.getLevel() != 0) .sorted(Comparator.comparing(TreeModel::getLevel)) .forEach(treeModel -> { TreeNode root = new TreeNode<>(); - convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS); + convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS, cards); trees.add(root); }); out.setTrees(trees); @@ -241,10 +250,13 @@ public class ResultService { private Uni getAllCombArray_(String uuid, MembreModel membreModel) { return registerRepository.list("competition.uuid = ?1", uuid) .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) + .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid) + .map(cards -> new Pair<>(registers, + matchModels.stream().map(m -> new MatchModelExtend(m, cards)).toList())))) + .map(pair -> { List registers = pair.getKey(); - List matchModels = pair.getValue(); + List matchModels = pair.getValue(); CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); @@ -290,11 +302,10 @@ public class ResultService { .toList(); builder.nb_insc(combs.size()); - builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); + builder.tt_match((int) matchModels.stream().filter(MatchModelExtend::isEnd).count()); builder.point(matchModels.stream() - .filter(MatchModel::isEnd) - .flatMap(m -> m.getScores().stream()) - .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .filter(MatchModelExtend::isEnd) + .flatMap(m -> m.getScoresToCompute().stream()) .mapToInt(s -> s.getS1() + s.getS2()).sum()); builder.combs(combs); @@ -345,7 +356,7 @@ public class ResultService { return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found"))); } - Uni> uni; + Uni> uni; if (id >= 0) { uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id, uuid, privacy).firstResult() @@ -360,9 +371,12 @@ public class ResultService { registerModel.getCategorie2().getName(trad)); return matchRepository.list( - "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + - "WHERE m.category.compet.uuid = ?1 AND (m.c1_id = ?2 OR m.c2_id = ?2 OR c1g = ?2 OR c2g = ?2)", - uuid, registerModel.getMembre()); + "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + + "WHERE m.category.compet.uuid = ?1 AND (m.c1_id = ?2 OR m.c2_id = ?2 OR c1g = ?2 OR c2g = ?2)", + uuid, registerModel.getMembre()) + .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid) + .map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards)) + .toList())); })); } else { uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult() @@ -373,14 +387,17 @@ public class ResultService { (guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad)); return matchRepository.list( - "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + - "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)", - uuid, guestModel); + "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + + "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)", + uuid, guestModel) + .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid) + .map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards)) + .toList())); }); } return uni.invoke(matchModels -> { - List pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct() + List pouleModels = matchModels.stream().map(MatchModelExtend::getCategory).distinct() .toList(); List matchs = new ArrayList<>(); @@ -388,7 +405,7 @@ public class ResultService { AtomicInteger sumPointMake = new AtomicInteger(0); AtomicInteger sumPointTake = new AtomicInteger(0); - for (MatchModel matchModel : matchModels) { + for (MatchModelExtend matchModel : matchModels) { if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) || (matchModel.getC2_id() == null && matchModel.getC2_guest() == null)) continue; @@ -405,35 +422,33 @@ public class ResultService { if (matchModel.isC1(id)) { builder2.adv(matchModel.getC2Name()); if (matchModel.isEnd()) { - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) + matchModel.getScoresToCompute() .forEach(scoreEntity -> { pointMake.addAndGet(scoreEntity.getS1()); pointTake.addAndGet(scoreEntity.getS2()); }); - builder2.score(matchModel.getScores().stream() + builder2.score(matchModel.getScoresToPrint().stream() .map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()); } else { builder2.score(new ArrayList<>()); } - builder2.win(matchModel.isEnd() && matchModel.win() > 0); + builder2.win(matchModel.isEnd() && matchModel.getWin() > 0); } else { builder2.adv(matchModel.getC1Name()); if (matchModel.isEnd()) { - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) + matchModel.getScoresToCompute() .forEach(scoreEntity -> { pointMake.addAndGet(scoreEntity.getS2()); pointTake.addAndGet(scoreEntity.getS1()); }); - builder2.score(matchModel.getScores().stream() + builder2.score(matchModel.getScoresToPrint().stream() .map(s -> new Integer[]{s.getS2(), s.getS1()}).toList()); } else { builder2.score(new ArrayList<>()); } - builder2.win(matchModel.isEnd() && matchModel.win() < 0); + builder2.win(matchModel.isEnd() && matchModel.getWin() < 0); } - builder2.eq(matchModel.isEnd() && matchModel.win() == 0); + builder2.eq(matchModel.isEnd() && matchModel.getWin() == 0); builder2.ratio( (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); @@ -525,9 +540,13 @@ public class ResultService { "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)", uuid, guests) - .map(matchModels -> - getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(), - matchModels, new ArrayList<>(), membreModel))); + + .chain(mm -> cardRepository.list("competition.uuid = ?1", uuid) + .map(cards -> + getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(), + mm.stream().map(m -> new MatchModelExtend(m, cards)).toList(), + new ArrayList<>(), membreModel) + ))); } else { return clubRepository.findById(id).chain(clubModel -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) @@ -535,14 +554,20 @@ public class ResultService { "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + "WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)", uuid, registers.stream().map(RegisterModel::getMembre).toList()) - .map(matchModels -> - getClubArray2(clubModel.getName(), - registers.stream().map(o -> (CombModel) o.getMembre()).toList(), - matchModels, registers, membreModel)))); + .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid) + .map(cards -> + getClubArray2(clubModel.getName(), + registers.stream().map(o -> (CombModel) o.getMembre()) + .toList(), + matchModels.stream() + .map(m -> new MatchModelExtend(m, cards)).toList(), + registers, membreModel) + + )))); } } - private ClubArrayData getClubArray2(String name, List combs, List matchModels, + private ClubArrayData getClubArray2(String name, List combs, List matchModels, List registers, MembreModel membreModel) { ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder(); builder.name(name); @@ -597,14 +622,14 @@ public class ResultService { return builder.build(); } - private static CombStat makeStat(List matchModels, CombModel comb) { + private static CombStat makeStat(List matchModels, CombModel comb) { CombStat stat = new CombStat(); matchModels.stream() .filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb))) .forEach(matchModel -> { stat.match_ids.add(matchModel.getId()); - int win = matchModel.win(); + int win = matchModel.getWin(); if (win == 0) { stat.score += 1; } else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) { @@ -615,8 +640,7 @@ public class ResultService { stat.l++; } - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) + matchModel.getScoresToCompute() .forEach(score -> { if (matchModel.isC1(comb)) { stat.pointMake += score.getS1(); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java index 0b04ba7..e99f786 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -1,7 +1,7 @@ package fr.titionfire.ffsaf.rest.data; -import fr.titionfire.ffsaf.data.model.MatchModel; import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.domain.entity.MatchModelExtend; import fr.titionfire.ffsaf.utils.ResultPrivacy; import fr.titionfire.ffsaf.utils.ScoreEmbeddable; import fr.titionfire.ffsaf.utils.TreeNode; @@ -43,16 +43,16 @@ public class ResultCategoryData { @RegisterForReflection public record PouleArrayData(String red, boolean red_w, List score, boolean blue_w, String blue, boolean eq, boolean end, Date date) { - public static PouleArrayData fromModel(MatchModel matchModel, MembreModel membreModel, ResultPrivacy privacy) { + public static PouleArrayData fromModel(MatchModelExtend matchModel, MembreModel membreModel, ResultPrivacy privacy) { return new PouleArrayData( matchModel.getC1Name(membreModel, privacy), - matchModel.isEnd() && matchModel.win() > 0, + matchModel.isEnd() && matchModel.getWin() > 0, matchModel.isEnd() ? - matchModel.getScores().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList() + matchModel.getScoresToPrint().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList() : new ArrayList<>(), - matchModel.isEnd() && matchModel.win() < 0, + matchModel.isEnd() && matchModel.getWin() < 0, matchModel.getC2Name(membreModel, privacy), - matchModel.isEnd() && matchModel.win() == 0, + matchModel.isEnd() && matchModel.getWin() == 0, matchModel.isEnd(), matchModel.getDate()); } @@ -60,10 +60,10 @@ public class ResultCategoryData { @RegisterForReflection public static record TreeData(long id, String c1FullName, String c2FullName, List scores, - boolean end) { - public static TreeData from(MatchModel match, MembreModel membreModel, ResultPrivacy privacy) { + boolean end, int win) { + public static TreeData from(MatchModelExtend match, MembreModel membreModel, ResultPrivacy privacy) { return new TreeData(match.getId(), match.getC1Name(membreModel, privacy), - match.getC2Name(membreModel, privacy), match.getScores(), match.isEnd()); + match.getC2Name(membreModel, privacy), match.getScoresToPrint(), match.isEnd(), match.getWin()); } } } diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js index e593364..8fbc892 100644 --- a/src/main/webapp/public/competition.js +++ b/src/main/webapp/public/competition.js @@ -90,22 +90,21 @@ function stopLoading(loading) { loading['root'].removeChild(loading['element']); } -function scoreToString(score) { - const scorePrint = (s1) => { - switch (s1) { - case -997: - return i18next.t('disc.'); - case -998: - return i18next.t('abs.'); - case -999: - return i18next.t('for.'); - case -1000: - return ""; - default: - return String(s1); - } +function scorePrint(s1) { + switch (s1) { + case -997: + return i18next.t('disc.'); + case -998: + return i18next.t('abs.'); + case -999: + return i18next.t('for.'); + case -1000: + return ""; + default: + return String(s1); } - +} +function scoreToString(score) { return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); } @@ -867,7 +866,7 @@ function drawGraph(root = []) { ctx.textBaseline = 'top'; for (let i = 0; i < scores.length; i++) { - const score = scores[i].s1 + "-" + scores[i].s2; + const score = scorePrint(scores[i].s1) + "-" + scorePrint(scores[i].s2); const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; const text = ctx.measureText(score); let dx = (size * 2 - text.width) / 2; @@ -945,20 +944,6 @@ function drawGraph(root = []) { if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); } - function win(scores) { - let sum = 0; - for (const score of scores) { - if (score.s1 === -1000 || score.s2 === -1000) - continue; - - if (score.s1 > score.s2) - sum++; - else if (score.s1 < score.s2) - sum--; - } - return sum; - } - let px = max_x; let py; let max_y @@ -971,7 +956,7 @@ function drawGraph(root = []) { for (const node of root) { let win_name = ""; if (node.data.end) { - if (win(node.data.scores) > 0) + if (node.data.win > 0) win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName; else win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName; -- 2.49.0 From cdd7221e86aa21ff86c263b3d8bef5f5a15fe2f7 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 30 Jan 2026 14:54:44 +0100 Subject: [PATCH 7/8] fix: add weight config for all register source --- src/main/webapp/src/pages/competition/CompetitionEdit.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx index 23fb7db..aed0e56 100644 --- a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx @@ -457,8 +457,7 @@ function Content({data}) { defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/> -
+
{t('poidsDemandéPour')} {CatList.map((cat, index) =>
Date: Fri, 30 Jan 2026 15:00:32 +0100 Subject: [PATCH 8/8] fix: ghost selection in PayAndValidateList --- src/main/webapp/src/pages/PayAndValidateList.jsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/webapp/src/pages/PayAndValidateList.jsx b/src/main/webapp/src/pages/PayAndValidateList.jsx index 23067e6..a3d5ce9 100644 --- a/src/main/webapp/src/pages/PayAndValidateList.jsx +++ b/src/main/webapp/src/pages/PayAndValidateList.jsx @@ -13,7 +13,6 @@ import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons"; import "./PayAndValidateList.css"; import * as Tools from "../utils/Tools.js"; import {useTranslation} from "react-i18next"; -import {counter} from "@fortawesome/fontawesome-svg-core"; export function PayAndValidateList({source}) { const {t} = useTranslation(); @@ -31,8 +30,7 @@ export function PayAndValidateList({source}) { const [lastSearch, setLastSearch] = useState(""); const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2); - const storedMembers = sessionStorage.getItem("selectedMembers"); - const [selectedMembers, setSelectedMembers] = useState(storedMembers ? JSON.parse(storedMembers) : []); + const [selectedMembers, setSelectedMembers] = useState([]); const setLoading = useLoadingSwitcher() const { @@ -41,10 +39,6 @@ export function PayAndValidateList({source}) { refresh } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1) - useEffect(() => { - sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers)); - }, [selectedMembers]); - useEffect(() => { refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`); }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]); -- 2.49.0