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."