From b1bcf75e566f4b742b93765857061edea0d91131 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 24 Nov 2025 15:56:15 +0100 Subject: [PATCH] feat: categorie on CM --- .../ffsaf/data/model/CardboardModel.java | 42 ++ .../ffsaf/data/model/CategoryModel.java | 2 + .../data/model/CompetitionGuestModel.java | 48 ++ .../ffsaf/data/model/CompetitionModel.java | 4 + .../ffsaf/data/model/MatchModel.java | 18 +- .../data/repository/CategoryRepository.java | 10 + .../CompetitionGuestRepository.java | 9 + .../data/repository/MatchRepository.java | 10 + .../ffsaf/data/repository/TreeRepository.java | 14 + .../ffsaf/domain/service/CategoryService.java | 28 +- .../domain/service/CompetPermService.java | 8 +- .../domain/service/CompetitionService.java | 6 +- .../ffsaf/domain/service/MatchService.java | 25 +- .../ffsaf/net2/data/CardboardEntity.java | 23 + .../ffsaf/net2/data/CombEntity.java | 40 ++ .../ffsaf/net2/data/MatchEntity.java | 56 +++ .../ffsaf/net2/data/TreeEntity.java | 71 +++ .../titionfire/ffsaf/rest/data/MatchData.java | 6 +- .../ffsaf/utils/CompetitionSystem.java | 2 +- .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 43 +- .../fr/titionfire/ffsaf/ws/PermLevel.java | 8 + .../titionfire/ffsaf/ws/recv/RCategorie.java | 231 +++++++++ .../titionfire/ffsaf/ws/recv/WSReceiver.java | 3 + src/main/webapp/src/hooks/useWS.jsx | 45 +- .../src/pages/competition/editor/CMAdmin.jsx | 445 ++++++++++++++++++ .../editor/CompetitionManagerRoot.jsx | 16 +- src/main/webapp/src/utils/MatchReducer.jsx | 44 ++ src/main/webapp/src/utils/TreeUtils.js | 129 +++++ 28 files changed, 1343 insertions(+), 43 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionGuestRepository.java create mode 100644 src/main/java/fr/titionfire/ffsaf/net2/data/CardboardEntity.java create mode 100644 src/main/java/fr/titionfire/ffsaf/net2/data/CombEntity.java create mode 100644 src/main/java/fr/titionfire/ffsaf/net2/data/MatchEntity.java create mode 100644 src/main/java/fr/titionfire/ffsaf/net2/data/TreeEntity.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/PermLevel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java create mode 100644 src/main/webapp/src/pages/competition/editor/CMAdmin.jsx create mode 100644 src/main/webapp/src/utils/MatchReducer.jsx create mode 100644 src/main/webapp/src/utils/TreeUtils.js diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java new file mode 100644 index 0000000..255f83e --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CardboardModel.java @@ -0,0 +1,42 @@ +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/CategoryModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java index d32dac0..5f381cf 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java @@ -42,4 +42,6 @@ public class CategoryModel { List tree; Integer type; + + String liceName = "1"; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java new file mode 100644 index 0000000..5defa6f --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java @@ -0,0 +1,48 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.Categorie; +import fr.titionfire.ffsaf.utils.Genre; +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 CompetitionGuestModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "competition", referencedColumnName = "id") + CompetitionModel competition; + + String lname = ""; + String fname = ""; + + Categorie categorie = null; + + String club = null; + + Genre genre = null; + + String country = "fr"; + + public CompetitionGuestModel(String s) { + this.fname = s.substring(0, s.indexOf(" ")); + this.lname = s.substring(s.indexOf(" ") + 1); + } + + public String getName() { + return fname + " " + lname; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java index 6fd106c..8834b92 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -55,6 +55,10 @@ public class CompetitionModel { @OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL) List insc; + @OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + List guests = new ArrayList<>(); + + List banMembre = new ArrayList<>(); String owner; 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 22765ea..ad2a032 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -7,6 +7,7 @@ import jakarta.persistence.*; import lombok.*; import java.util.ArrayList; +import java.util.Date; import java.util.List; @Getter @@ -32,13 +33,17 @@ public class MatchModel { @JoinColumn(name = "c1", referencedColumnName = "id") MembreModel c1_id = null; - String c1_str = null; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "c1_guest", referencedColumnName = "id") + CompetitionGuestModel c1_guest = null; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "c2", referencedColumnName = "id") MembreModel c2_id = null; - String c2_str = null; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "c2_guest", referencedColumnName = "id") + CompetitionGuestModel c2_guest = null; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "id_category", referencedColumnName = "id") @@ -48,22 +53,27 @@ public class MatchModel { boolean isEnd = true; + Date date = null; + @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "score", joinColumns = @JoinColumn(name = "id_match")) List scores = new ArrayList<>(); char poule = 'A'; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "match", referencedColumnName = "id") + List cardboard; public String getC1Name() { if (c1_id == null) - return c1_str; + return c1_guest.fname + " " + c1_guest.lname; return c1_id.fname + " " + c1_id.lname; } public String getC2Name() { if (c2_id == null) - return c2_str; + return c2_guest.fname + " " + c2_guest.lname; return c2_id.fname + " " + c2_id.lname; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CategoryRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CategoryRepository.java index 8271b3b..eb3d5b9 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/repository/CategoryRepository.java +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/CategoryRepository.java @@ -1,9 +1,19 @@ package fr.titionfire.ffsaf.data.repository; import fr.titionfire.ffsaf.data.model.CategoryModel; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class CategoryRepository implements PanacheRepositoryBase { + + public Uni create(CategoryModel categoryModel) { + categoryModel.setSystem(CompetitionSystem.INTERNAL); + return Panache.withTransaction(() -> this.persist(categoryModel) + .invoke(categoryModel1 -> categoryModel1.setSystemId(categoryModel1.getId()))) + .chain(this::persist); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionGuestRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionGuestRepository.java new file mode 100644 index 0000000..6e0a959 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/CompetitionGuestRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CompetitionGuestRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java index ab284c4..c2bbe97 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java @@ -1,9 +1,19 @@ package fr.titionfire.ffsaf.data.repository; import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.utils.CompetitionSystem; +import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class MatchRepository implements PanacheRepositoryBase { + + public Uni create(MatchModel matchModel) { + matchModel.setSystem(CompetitionSystem.INTERNAL); + return Panache.withTransaction(() -> this.persistAndFlush(matchModel) + .invoke(matchModel1 -> matchModel1.setSystemId(matchModel1.getId()))) + .chain(this::persist); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java index 57bb22b..e38aee0 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/TreeRepository.java @@ -2,8 +2,22 @@ package fr.titionfire.ffsaf.data.repository; import fr.titionfire.ffsaf.data.model.TreeModel; import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; +import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class TreeRepository implements PanacheRepositoryBase { + + @WithTransaction + public Uni deleteTree(TreeModel entity) { + Uni uni = Uni.createFrom().item(false); + if (entity == null) + return uni; + if (entity.getLeft() != null) + uni = uni.chain(__ -> this.deleteTree(entity.getLeft())); + if (entity.getRight() != null) + uni = uni.chain(__ -> this.deleteTree(entity.getRight())); + return uni.chain(__ -> this.deleteById(entity.getId())); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java index 19cbee0..0485ede 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java @@ -1,9 +1,6 @@ package fr.titionfire.ffsaf.domain.service; -import fr.titionfire.ffsaf.data.model.MatchModel; -import fr.titionfire.ffsaf.data.model.MembreModel; -import fr.titionfire.ffsaf.data.model.CategoryModel; -import fr.titionfire.ffsaf.data.model.TreeModel; +import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.rest.data.CategoryData; import fr.titionfire.ffsaf.rest.data.CategoryFullData; @@ -45,10 +42,13 @@ public class CategoryService { @Inject CompetPermService permService; + @Inject + CompetitionGuestRepository competitionGuestRepository; + public Uni getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) { return repository.find("systemId = ?1 AND system = ?2", id, system) .firstResult() - .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .onItem().ifNull().failWith(() -> new RuntimeException("Category not found")) .call(data -> permService.hasAdminViewPerm(securityCtx, data.getCompet())) .map(CategoryData::fromModel); } @@ -171,6 +171,15 @@ public class CategoryService { .invoke(o2 -> o2.forEach(m -> o.membres.put(m.getId(), m))) ) ) + .call(o -> Mutiny.fetch(o.category.getCompet().getGuests()) + .invoke(o2 -> o2.forEach(m -> o.guest.put(m.getFname() + " " + m.getLname(), m))) + .map(o2 -> data.getMatches().stream().flatMap(m -> Stream.of(m.getC1_str(), m.getC2_str()) + .filter(Objects::nonNull)).distinct().filter(s -> !o.guest.containsKey(s)).map( + CompetitionGuestModel::new).toList()) + .call(o3 -> o3.isEmpty() ? Uni.createFrom().nullItem() : + Uni.join().all(o3.stream().map(o4 -> competitionGuestRepository.persist(o4)).toList()) + .andFailFast()) + .invoke(o2 -> o2.forEach(m -> o.guest.put(m.getFname() + " " + m.getLname(), m)))) .invoke(in -> { ArrayList node = new ArrayList<>(); for (TreeModel treeModel : in.category.getTree()) @@ -214,8 +223,8 @@ public class CategoryService { } mm.setCategory(in.category); mm.setCategory_ord(m.getCategory_ord()); - mm.setC1_str(m.getC1_str()); - mm.setC2_str(m.getC2_str()); + mm.setC1_guest(in.guest.getOrDefault(m.getC1_str(), null)); + mm.setC2_guest(in.guest.getOrDefault(m.getC2_str(), null)); mm.setC1_id(in.membres.getOrDefault(m.getC1_id(), null)); mm.setC2_id(in.membres.getOrDefault(m.getC2_id(), null)); mm.setEnd(m.isEnd()); @@ -238,6 +247,7 @@ public class CategoryService { private static class WorkData { CategoryModel category; HashMap membres = new HashMap<>(); + HashMap guest = new HashMap<>(); List match = new ArrayList<>(); List toRmMatch; List unlinkNode; @@ -246,7 +256,7 @@ public class CategoryService { public Uni delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) { return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() - .onItem().ifNull().failWith(() -> new RuntimeException("Poule not found")) + .onItem().ifNull().failWith(() -> new RuntimeException("Category not found")) .call(o -> permService.hasEditPerm(securityCtx, o.getCompet())) .call(o -> Mutiny.fetch(o.getTree()) .call(o2 -> o2.isEmpty() ? Uni.createFrom().nullItem() : @@ -260,7 +270,7 @@ public class CategoryService { Panache.withTransaction(() -> treeRepository.delete("id IN ?1", in))) ) ) - .call(o -> matchRepository.delete("poule.id = ?1", o.getId())) + .call(o -> matchRepository.delete("category.id = ?1", o.getId())) .chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId()))); } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java index 23a7bd6..1996bb2 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java @@ -89,7 +89,7 @@ public class CompetPermService { .onFailure().call(throwable -> cacheAccess.invalidate(securityCtx.getSubject())); Uni> none = cacheNoneAccess.getAsync(securityCtx.getSubject(), - k -> competitionRepository.list("system = ?1", CompetitionSystem.NONE) + k -> competitionRepository.list("system = ?1", CompetitionSystem.INTERNAL) .map(competitionModels -> { HashMap map = new HashMap<>(); for (CompetitionModel model : competitionModels) { @@ -184,7 +184,7 @@ public class CompetPermService { if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here throw new DForbiddenException(); - if (o.getSystem() == CompetitionSystem.NONE) + if (o.getSystem() == CompetitionSystem.INTERNAL) if (securityCtx.roleHas("club_president") || securityCtx.roleHas("club_respo_intra") || securityCtx.roleHas("club_secretaire") || securityCtx.roleHas("club_tresorier")) return Uni.createFrom().nullItem(); @@ -219,7 +219,7 @@ public class CompetPermService { if (o.getSystem() == CompetitionSystem.SAFCA) return hasSafcaEditPerm(securityCtx, o.getId()); - if (o.getSystem() == CompetitionSystem.NONE) { + if (o.getSystem() == CompetitionSystem.INTERNAL) { if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin()) return Uni.createFrom().nullItem(); @@ -259,7 +259,7 @@ public class CompetPermService { if (o.getSystem() == CompetitionSystem.SAFCA) return hasSafcaTablePerm(securityCtx, o.getId()); - if (o.getSystem() == CompetitionSystem.NONE) { + if (o.getSystem() == CompetitionSystem.INTERNAL) { if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin()) return Uni.createFrom().nullItem(); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index 52c0db5..76f62f5 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -103,7 +103,7 @@ public class CompetitionService { if (id == 0) { return Uni.createFrom() .item(new CompetitionData(null, "", "", "", "", new Date(), new Date(), - CompetitionSystem.NONE, RegisterMode.FREE, new Date(), new Date(), true, + CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true, null, "", "", null, true, "", "", "", "")); } return permService.hasAdminViewPerm(securityCtx, id) @@ -184,7 +184,7 @@ public class CompetitionService { }).map(CompetitionData::fromModel) .call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()) - .call(c -> (c.getSystem() == CompetitionSystem.NONE) ? cacheNoneAccess.invalidate( + .call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()); } else { return permService.hasEditPerm(securityCtx, data.getId()) @@ -208,7 +208,7 @@ public class CompetitionService { }).map(CompetitionData::fromModel) .call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()) - .call(c -> (c.getSystem() == CompetitionSystem.NONE) ? cacheNoneAccess.invalidate( + .call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java index 279d43d..71dda91 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MatchService.java @@ -1,9 +1,11 @@ package fr.titionfire.ffsaf.domain.service; +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; import fr.titionfire.ffsaf.data.model.MatchModel; -import fr.titionfire.ffsaf.data.repository.CombRepository; -import fr.titionfire.ffsaf.data.repository.MatchRepository; import fr.titionfire.ffsaf.data.repository.CategoryRepository; +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.rest.data.MatchData; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.utils.CompetitionSystem; @@ -33,6 +35,9 @@ public class MatchService { @Inject CompetPermService permService; + @Inject + CompetitionGuestRepository competitionGuestRepository; + public Uni getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) { return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() .onItem().ifNull().failWith(() -> new DNotFoundException("Match not found")) @@ -75,8 +80,6 @@ public class MatchService { } ) .chain(o -> { - o.setC1_str(data.getC1_str()); - o.setC2_str(data.getC2_str()); o.setCategory_ord(data.getCategory_ord()); o.getScores().clear(); o.getScores().addAll(data.getScores()); @@ -88,6 +91,20 @@ public class MatchService { .chain(() -> (data.getC1_id() == null) ? Uni.createFrom().nullItem() : combRepository.findById(data.getC2_id())) .invoke(o::setC2_id) + .chain(() -> (data.getC1_str() == null) ? + Uni.createFrom() + .item((CompetitionGuestModel) null) : competitionGuestRepository.find( + "fname = ?1 AND lname = ?2", + data.getC1_str().substring(0, data.getC1_str().indexOf(" ")), + data.getC1_str().substring(data.getC1_str().indexOf(" ") + 1)).firstResult()) + .invoke(o::setC1_guest) + .chain(() -> (data.getC2_str() == null) ? + Uni.createFrom() + .item((CompetitionGuestModel) null) : competitionGuestRepository.find( + "fname = ?1 AND lname = ?2", + data.getC2_str().substring(0, data.getC2_str().indexOf(" ")), + data.getC2_str().substring(data.getC2_str().indexOf(" ") + 1)).firstResult()) + .invoke(o::setC2_guest) .chain(() -> Panache.withTransaction(() -> repository.persist(o))); }) .map(MatchData::fromModel); diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/CardboardEntity.java b/src/main/java/fr/titionfire/ffsaf/net2/data/CardboardEntity.java new file mode 100644 index 0000000..44b1a98 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/CardboardEntity.java @@ -0,0 +1,23 @@ +package fr.titionfire.ffsaf.net2.data; + +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().getId(), model.getMatch().getId(), model.getCompet().getId(), + model.getRed(), model.getYellow()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/CombEntity.java b/src/main/java/fr/titionfire/ffsaf/net2/data/CombEntity.java new file mode 100644 index 0000000..07a825d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/CombEntity.java @@ -0,0 +1,40 @@ +package fr.titionfire.ffsaf.net2.data; + +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.utils.Categorie; +import fr.titionfire.ffsaf.utils.Genre; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class CombEntity { + private long id; + private String lname = ""; + private String fname = ""; + Categorie categorie = null; + Long club = null; + String club_str = null; + Genre genre = null; + String country = "fr"; + + public static CombEntity fromModel(MembreModel model) { + if (model == null) + return null; + + return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(), + model.getClub().getId(), model.getClub().getName(), model.getGenre(), model.getCountry()); + } + + + public static CombEntity fromModel(CompetitionGuestModel model) { + if (model == null) + return null; + + return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null, + model.getClub(), model.getGenre(), model.getCountry()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/MatchEntity.java b/src/main/java/fr/titionfire/ffsaf/net2/data/MatchEntity.java new file mode 100644 index 0000000..360ef02 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/MatchEntity.java @@ -0,0 +1,56 @@ +package fr.titionfire.ffsaf.net2.data; + +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class MatchEntity { + private long id; + private CombEntity c1; + private CombEntity c2; + private long categorie_ord = 0; + private boolean isEnd; + private long categorie; + private Date date; + private List scores; + private char poule; + private List cardboard; + + public static MatchEntity fromModel(MatchModel model) { + if (model == null) + return null; + return new MatchEntity(model.getId(), + (model.getC1_id() == null) ? CombEntity.fromModel(model.getC1_guest()) : CombEntity.fromModel( + model.getC1_id()), + (model.getC2_id() == null) ? CombEntity.fromModel(model.getC2_guest()) : CombEntity.fromModel( + 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; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/TreeEntity.java b/src/main/java/fr/titionfire/ffsaf/net2/data/TreeEntity.java new file mode 100644 index 0000000..493fac1 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/TreeEntity.java @@ -0,0 +1,71 @@ +package fr.titionfire.ffsaf.net2.data; + +import fr.titionfire.ffsaf.data.model.TreeModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class TreeEntity { + private Long id; + private Long categorie; + private Integer level; + private MatchEntity match; + private TreeEntity left; + private TreeEntity right; + private TreeEntity associatedNode; + + public static TreeEntity fromModel(TreeModel model) { + if (model == null) + return null; + + return new TreeEntity(model.getId(), model.getCategory(), model.getLevel(), MatchEntity.fromModel(model.getMatch()), fromModel(model.getLeft()), + fromModel(model.getRight()), null); + } + + public TreeEntity getMatchNode(Long matchId) { + if (this.match != null && this.match.getId() == matchId) { + return this; + } else { + if (this.left != null) { + TreeEntity left = this.left.getMatchNode(matchId); + if (left != null) { + return left; + } + } + if (this.right != null) { + TreeEntity right = this.right.getMatchNode(matchId); + if (right != null) { + return right; + } + } + } + return null; + } + + public static TreeEntity getParent(TreeEntity current, TreeEntity target) { + if (current == null) { + return null; + } else if (current.equals(target)) { + return null; + } else if (target.equals(current.left) || target.equals(current.right)) { + return current; + } else { + TreeEntity left = getParent(current.left, target); + if (left != null) + return left; + return getParent(current.right, target); + } + } + + public static void setAssociated(TreeEntity current, TreeEntity next) { + if (current == null || next == null) { + return; + } + current.setAssociatedNode(next); + setAssociated(current.getLeft(), next.getLeft()); + setAssociated(current.getRight(), next.getRight()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java index 143d0ad..7b13012 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/MatchData.java @@ -28,8 +28,10 @@ public class MatchData { return null; return new MatchData(model.getSystemId(), - (model.getC1_id() == null) ? null : model.getC1_id().getId(), model.getC1_str(), - (model.getC2_id() == null) ? null : model.getC2_id().getId(), model.getC2_str(), + (model.getC1_id() == null) ? null : model.getC1_id().getId(), + (model.getC1_guest() == null) ? null : model.getC1_guest().getName(), + (model.getC2_id() == null) ? null : model.getC2_id().getId(), + (model.getC2_guest() == null) ? null : model.getC2_guest().getName(), model.getCategory().getId(), model.getCategory_ord(), model.isEnd(), model.getPoule(), model.getScores()); } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java b/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java index 32cb79a..a183d81 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/CompetitionSystem.java @@ -1,5 +1,5 @@ package fr.titionfire.ffsaf.utils; public enum CompetitionSystem { - SAFCA, NONE + SAFCA, INTERNAL } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index 5de5908..ff1ede9 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.domain.service.CompetPermService; import fr.titionfire.ffsaf.net2.MessageType; import fr.titionfire.ffsaf.utils.SecurityCtx; import fr.titionfire.ffsaf.ws.data.WelcomeInfo; +import fr.titionfire.ffsaf.ws.recv.RCategorie; import fr.titionfire.ffsaf.ws.recv.RMatch; import fr.titionfire.ffsaf.ws.recv.WSReceiver; import fr.titionfire.ffsaf.ws.send.JsonUni; @@ -22,9 +23,7 @@ import org.jboss.logging.Logger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; @@ -38,6 +37,9 @@ public class CompetitionWS { @Inject RMatch rMatch; + @Inject + RCategorie rCategorie; + @Inject SecurityCtx securityCtx; @@ -69,6 +71,7 @@ public class CompetitionWS { @PostConstruct void init() { getWSReceiverMethods(RMatch.class, rMatch); + getWSReceiverMethods(RCategorie.class, rCategorie); } @OnOpen @@ -82,12 +85,12 @@ public class CompetitionWS { if (cm == null) throw new ForbiddenException(); })) - .call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> "admin") + .call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> PermLevel.ADMIN) .onFailure() - .recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> "table")) + .recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> PermLevel.TABLE)) .onFailure() - .recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> "view")) - .invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem)) + .recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW)) + .invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem.toString())) .invoke(prem -> LOGGER.infof("Connection permission: %s", prem)) .onFailure().transform(t -> new ForbiddenException())) .invoke(__ -> { @@ -150,12 +153,19 @@ public class CompetitionWS { try { for (Map.Entry entry : wsMethods.entrySet()) { Method method = entry.getKey(); - if (method.getAnnotation(WSReceiver.class).code().equalsIgnoreCase(message.code())) { + WSReceiver wsReceiver = method.getAnnotation(WSReceiver.class); + PermLevel perm = PermLevel.valueOf(connection.userData().get(UserData.TypedKey.forString("prem"))); + if (wsReceiver.code().equalsIgnoreCase(message.code())) { + if (wsReceiver.permission().ordinal() > perm.ordinal()) + return Uni.createFrom().item(makeError(message, "Permission denied")).toMulti(); return ((Uni) method.invoke(entry.getValue(), connection, MAPPER.treeToValue(message.data(), method.getParameterTypes()[1]))) .map(o -> makeReply(message, o)) .onFailure() - .recoverWithItem(t -> makeError(message, t.getMessage())).toMulti() + .recoverWithItem(t -> { + LOGGER.error(t.getMessage(), t); + return makeError(message, t.getMessage()); + }).toMulti() .filter(__ -> message.type() == MessageType.REQUEST); } } @@ -168,6 +178,21 @@ public class CompetitionWS { // return Uni.createFrom().item(new Message<>(message.uuid(), message.code(), MessageType.REPLY, "ko")); } + public static Uni sendNotifyToOtherEditor(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 -> { + if (uuid.equals(c.pathParam("uuid"))) { + queue.add(c.sendText(new MessageOut(UUID.randomUUID(), code, MessageType.NOTIFY, data))); + } + }); + + return Uni.join().all(queue).andCollectFailures().onFailure().recoverWithNull().replaceWithVoid(); + } + @OnError Uni error(WebSocketConnection connection, ForbiddenException t) { return connection.close(CloseReason.INTERNAL_SERVER_ERROR); diff --git a/src/main/java/fr/titionfire/ffsaf/ws/PermLevel.java b/src/main/java/fr/titionfire/ffsaf/ws/PermLevel.java new file mode 100644 index 0000000..3e7a2e6 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/PermLevel.java @@ -0,0 +1,8 @@ +package fr.titionfire.ffsaf.ws; + +public enum PermLevel { + NONE, + VIEW, + TABLE, + ADMIN, +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java new file mode 100644 index 0000000..58d1132 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -0,0 +1,231 @@ +package fr.titionfire.ffsaf.ws.recv; + +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.CategoryRepository; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.data.repository.TreeRepository; +import fr.titionfire.ffsaf.net2.data.MatchEntity; +import fr.titionfire.ffsaf.net2.data.TreeEntity; +import fr.titionfire.ffsaf.rest.exception.DNotFoundException; +import fr.titionfire.ffsaf.utils.TreeNode; +import fr.titionfire.ffsaf.ws.CompetitionWS; +import fr.titionfire.ffsaf.ws.PermLevel; +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 org.hibernate.reactive.mutiny.Mutiny; + +import java.util.ArrayList; +import java.util.List; + +@WithSession +@ApplicationScoped +@RegisterForReflection +public class RCategorie { + //private static final Logger LOGGER = Logger.getLogger(RCategorie.class); + + @Inject + CategoryRepository categoryRepository; + + @Inject + CompetitionRepository competitionRepository; + + @Inject + MatchRepository matchRepository; + + @Inject + TreeRepository treeRepository; + + @WSReceiver(code = "getAllCategory", permission = PermLevel.VIEW) + public Uni> getAllCategory(WebSocketConnection connection, Object o) { + return categoryRepository.list("compet.uuid", connection.pathParam("uuid")) + .map(category -> category.stream().map(JustCategorie::from).toList()); + } + + @WSReceiver(code = "getFullCategory", permission = PermLevel.VIEW) + public Uni getFullCategory(WebSocketConnection connection, Long id) { + FullCategory fullCategory = new FullCategory(); + + return categoryRepository.findById(id) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException("Catégorie non trouver"); + })) + .invoke(cat -> { + fullCategory.setId(cat.getId()); + fullCategory.setName(cat.getName()); + fullCategory.setLiceName(cat.getLiceName()); + fullCategory.setType(cat.getType()); + }) + .call(cat -> Mutiny.fetch(cat.getMatchs()) + .map(matchModels -> matchModels.stream().filter(o -> o.getCategory_ord() >= 0) + .map(MatchEntity::fromModel).toList()) + .invoke(fullCategory::setMatches)) + .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId()) + .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) + .invoke(fullCategory::setTrees)) + .map(__ -> fullCategory); + } + + @WSReceiver(code = "createCategory", permission = PermLevel.ADMIN) + public Uni createCategory(WebSocketConnection connection, JustCategorie categorie) { + return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult() + .chain(cm -> { + CategoryModel categoryModel = new CategoryModel(); + categoryModel.setName(categorie.name); + categoryModel.setCompet(cm); + categoryModel.setMatchs(new ArrayList<>()); + categoryModel.setTree(new ArrayList<>()); + categoryModel.setType(categorie.type); + categoryModel.setLiceName(categorie.liceName); + + return categoryRepository.create(categoryModel); + }) + .call(cat -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendAddCategory", + JustCategorie.from(cat))) + .map(CategoryModel::getId); + } + + @WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN) + public Uni updateCategory(WebSocketConnection connection, JustCategorie categorie) { + return categoryRepository.findById(categorie.id) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException("Catégorie non trouver"); + })) + .chain(cat -> { + cat.setName(categorie.name); + cat.setLiceName(categorie.liceName); + cat.setType(categorie.type); + return Panache.withTransaction(() -> categoryRepository.persist(cat)); + }) + .call(cat -> { + Uni uni = Uni.createFrom().nullItem(); + if ((categorie.type() & 1) == 0) + uni = uni.chain(__ -> matchRepository.delete("category = ?1 AND category_ord >= 0", cat)); + if ((categorie.type() & 2) == 0) { + uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId())) + .chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat)); + } + return uni; + }) + .call(cat -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendCategory", JustCategorie.from(cat))) + .replaceWithVoid(); + } + + private Uni getOrCreateTreeNode(Long id, Long categorieId, int level) { + if (id == null) { + TreeModel treeModel = new TreeModel(); + treeModel.setCategory(categorieId); + treeModel.setLevel(level); + + return Panache.withTransaction(() -> treeRepository.persistAndFlush(treeModel)); + } else { + return Panache.withTransaction( + () -> treeRepository.find("match.id = ?1", id).firstResult() + .invoke(t -> t.setLevel(level)) + .chain(t -> treeRepository.persist(t))); + } + } + + private Uni updateNode(TreeNode current, TreeModel currentTreeModel, CategoryModel category) { + return Uni.createFrom().item(currentTreeModel) + .chain(t -> { + if (current.getData() != null) + return Uni.createFrom().item(t); + + MatchModel matchModel = new MatchModel(); + matchModel.setCategory(category); + matchModel.setCategory_ord(-42); + matchModel.setEnd(false); + + return matchRepository.create(matchModel).onItem() + .invoke(t::setMatch) + .chain(__ -> treeRepository.persistAndFlush(t)); + }) + .call(t -> { + if (current.getLeft() == null) + return Uni.createFrom().item(t); + + return getOrCreateTreeNode(current.getLeft().getData(), category.getId(), 0) + .invoke(t::setLeft) + .call(treeModel -> updateNode(current.getLeft(), treeModel, category)); + }) + .call(t -> { + if (current.getRight() == null) + return Uni.createFrom().item(t); + + return getOrCreateTreeNode(current.getRight().getData(), category.getId(), 0) + .invoke(t::setRight) + .call(treeModel -> updateNode(current.getRight(), treeModel, category)); + }) + .chain(t -> treeRepository.persist(t)) + .replaceWithVoid(); + } + + + @WSReceiver(code = "updateTrees", permission = PermLevel.ADMIN) + public Uni updateTrees(WebSocketConnection connection, TreeUpdate data) { + return categoryRepository.findById(data.categoryId) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException("Catégorie non trouver"); + })) + .call(cat -> treeRepository.update("level = -1, left = NULL, right = NULL WHERE category = ?1", + cat.getId())) + .call(cat -> { + Uni uni = Uni.createFrom().voidItem(); + + for (int i = 0; i < data.trees().size(); i++) { + TreeNode current = data.trees().get(i); + + int finalI = i; + uni = uni.chain(() -> getOrCreateTreeNode(current.getData(), cat.getId(), finalI + 1) + .call(treeModel -> updateNode(current, treeModel, cat))); + + } + Uni finalUni = uni; + return Panache.withTransaction(() -> finalUni); + }) + .call(cat -> treeRepository.list("category = ?1 AND level = -1", cat.getId()) + .map(l -> l.stream().map(o -> o.getMatch().getId()).toList()) + .call(__ -> treeRepository.delete("category = ?1 AND level = -1", cat.getId())) + .call(ids -> matchRepository.delete("id IN ?1", ids))) + .call(__ -> treeRepository.flush()) + .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId()) + .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) + .chain(trees -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", trees))) + .replaceWithVoid(); + } + + @RegisterForReflection + public record JustCategorie(long id, String name, int type, String liceName) { + static JustCategorie from(CategoryModel m) { + return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName()); + } + } + + @RegisterForReflection + public record TreeUpdate(long categoryId, List> trees) { + } + + @Data + @RegisterForReflection + public static class FullCategory { + long id; + String name; + int type; + String liceName; + List trees = null; + List matches; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java index 183b455..c901563 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/WSReceiver.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.ws.recv; +import fr.titionfire.ffsaf.ws.PermLevel; import io.quarkus.runtime.annotations.RegisterForReflection; import java.lang.annotation.Retention; @@ -11,4 +12,6 @@ public @interface WSReceiver { String code(); + PermLevel permission() default PermLevel.VIEW; + } diff --git a/src/main/webapp/src/hooks/useWS.jsx b/src/main/webapp/src/hooks/useWS.jsx index 3ef5f9d..8d3fbd3 100644 --- a/src/main/webapp/src/hooks/useWS.jsx +++ b/src/main/webapp/src/hooks/useWS.jsx @@ -1,4 +1,6 @@ import {createContext, useContext, useEffect, useId, useReducer, useRef, useState} from "react"; +import {apiAxios} from "../utils/Tools.js"; +import {toast} from "react-toastify"; function uuidv4() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => @@ -45,6 +47,11 @@ export function WSProvider({url, onmessage, children}) { const ws = useRef(null) const listenersRef = useRef([]) const callbackRef = useRef({}) + const isReadyRef = useRef(isReady) + + useEffect(() => { + isReadyRef.current = isReady + }, [isReady]) useEffect(() => { listenersRef.current = state.listener @@ -129,7 +136,7 @@ export function WSProvider({url, onmessage, children}) { const send = (uuid, code, type, data, resolve = () => { }, reject = () => { }) => { - if (!isReady) { + if (!isReadyRef.current) { reject("WebSocket is not connected"); return; } @@ -150,6 +157,8 @@ export function WSProvider({url, onmessage, children}) { } } + + console.log("WSProvider: sending message", {uuid, code, type, data}); ws.current?.send(JSON.stringify({ uuid: uuid, code: code, @@ -191,3 +200,37 @@ export function useWS() { send, } } + +export function useRequestWS(code, payload, setLoading = null, loadingLevel = 1) { + const [data, setData] = useState(null) + const [error, setErrors] = useState(null) + const {isReady, sendRequest} = useWS() + + const refresh = (code, payload) => { + if (setLoading) + setLoading(loadingLevel) + + sendRequest(code, payload).then((data) => { + setData(data); + }).catch((err) => { + setErrors(err); + }).finally(() => { + if (setLoading) + setLoading(0) + }) + } + + useEffect(() => { + if (isReady) + refresh(code, payload) + else{ + if (setLoading) + setLoading(loadingLevel) + setTimeout(() => refresh(code, payload), 1000) + } + }, []); + + return { + data, error, refresh, setData + } +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx new file mode 100644 index 0000000..1066ed7 --- /dev/null +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -0,0 +1,445 @@ +import {useEffect, useReducer, useRef, useState} from "react"; +import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; +import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {toast} from "react-toastify"; +import {build_tree, from_sendTree, resize_tree, TreeNode} from "../../../utils/TreeUtils.js" +import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; +import {SimpleReducer} from "../../../utils/SimpleReducer.jsx"; +import {MarchReducer} from "../../../utils/MatchReducer.jsx"; + +export function CMAdmin() { + const [catId, setCatId] = useState(null); + const [cat, setCat] = useState(null); + const [combs, setCombs] = useState([]) + // const [cats, setCats] = useState([]) + + const {dispatch} = useWS(); + + useEffect(() => { + const categoryListener = ({data}) => { + if (!cat || data.id !== cat.id) + return + setCat({ + ...cat, + name: data.name, + liceName: data.liceName, + type: data.type + }) + } + dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) + return () => dispatch({type: 'removeListener', payload: categoryListener}) + }, []); + + /*useEffect(() => { + toast.promise(sendRequest("getAllCategory", {}), + { + pending: 'Chargement des catégories...', + success: 'Catégories chargées !', + error: 'Erreur lors du chargement des catégories' + } + ).then((data) => { + setCats(data); + }) + }, []);*/ + + return <> +
+
+ +
+ +
+ +
+ +
+
+
+
+ +} + +function CategoryHeader({cat, setCatId}) { + const setLoading = useLoadingSwitcher() + const bthRef = useRef(); + const confirmRef = useRef(); + const [modal, setModal] = useState({}) + const [confirm, setConfirm] = useState({}) + + const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); + const {dispatch} = useWS(); + + useEffect(() => { + const categoryListener = ({data}) => { + setCats([ + ...cats.filter(c => c.id !== data.id), + data + ]) + } + dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) + return () => dispatch({type: 'removeListener', payload: categoryListener}) + }, [cats]); + + useEffect(() => { + if (cats && cats.length > 0 && !cat) { + setCatId(cats[0].id); + } + }, [cats]); + + const handleCatChange = (e) => { + const selectedCatId = e.target.value; + if (selectedCatId !== "-1") { + setCatId(selectedCatId); + } else { // New category + setModal({}); + bthRef.current.click(); + console.log(cat); + e.target.value = cat?.id; + } + } + + return
+
+
+
Edition de la catégorie
+ +
+
+
+
+
+ +
+ + + + + + + { + }} onCancel={confirm.cancel ? confirm.cancel : () => { + }} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/> +
+} + +function ModalContent({state, setCatId, setConfirm, confirmRef}) { + const [name, setName] = useState("") + const [lice, setLice] = useState("1") + const [poule, setPoule] = useState(true) + const [tournoi, setTournoi] = useState(false) + const [size, setSize] = useState(4) + const [loserMatch, setLoserMatch] = useState(1) + + const {sendRequest} = useWS(); + + useEffect(() => { + setName(state.name || ""); + setLice(state.liceName || "1"); + setPoule(((state.type || 1) & 1) !== 0); + setTournoi((state.type & 2) !== 0); + + if (state?.trees && state.trees.length >= 1) { + const tree = state.trees[0]; + setSize(tree.getMaxChildrenAtDepth(tree.death() - 1) * 2); + + if (state.trees.length === 1) { + setLoserMatch(0); + } else if (state.trees.length === 2) { + setLoserMatch(1); + } else { + setLoserMatch(-1); + } + } else { + setSize(4); + setLoserMatch(1); + } + }, [state]) + + const handleSubmit = (e) => { + e.preventDefault(); + + const regex = /^([^;]+;)*[^;]+$/; + if (regex.test(lice.trim()) === false) { + toast.error("Le format du nom des lices est invalide. Veuillez séparer les noms par des ';'."); + return; + } + + const nType = (poule ? 1 : 0) + (tournoi ? 2 : 0); + if (nType === 0) { + toast.error("Au moins un type (poule ou tournoi) doit être sélectionné."); + return; + } + + if (state?.id) { + const applyChanges = () => { + const newData = { + id: state.id, + name: name.trim(), + liceName: lice.trim(), + type: nType + } + + let nbMatch = -1; + let oldSubTree = -1; + const oldTrees = state?.trees || []; + if (oldTrees.length >= 1) { + const tree = state.trees[0]; + nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1); + if (state.trees.length === 1) + oldSubTree = 0 + else if (state.trees.length === 2) + oldSubTree = 1 + } + + console.log(tournoi, size, nbMatch, loserMatch, oldSubTree); + if (tournoi && (size !== nbMatch * 2 || loserMatch !== oldSubTree)) { + setConfirm({ + title: "Changement de l'arbre du tournoi", + message: `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)!`, + confirm: () => { + const trees2 = build_tree(size, loserMatch) + const newTrees = [] + + let i = 0; + for (; i < oldTrees.length && i < trees2.length; i++) { + newTrees.push(oldTrees[i]); + resize_tree(newTrees.at(i), trees2.at(i)); + } + + for (; i < trees2.length; i++) { + newTrees.push(trees2.at(i)); + } + + toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), + { + pending: 'Mise à jour des arbres du tournoi...', + success: 'Arbres mis à jour !', + error: 'Erreur lors de la mise à jour des arbres' + } + ).then(__ => { + toast.promise(sendRequest('updateCategory', newData), + { + pending: 'Mise à jour de la catégorie...', + success: 'Catégorie mise à jour !', + error: 'Erreur lors de la mise à jour de la catégorie' + } + ) + }) + } + }) + confirmRef.current.click(); + } else { + toast.promise(sendRequest('updateCategory', newData), + { + pending: 'Mise à jour de la catégorie...', + success: 'Catégorie mise à jour !', + error: 'Erreur lors de la mise à jour de la catégorie' + } + ) + } + } + + if (nType !== state.type) { + let typeStr = ""; + if ((state.type & 1) !== 0 && (nType & 1) === 0) + typeStr += "poule "; + if ((state.type & 2) !== 0 && (nType & 2) === 0) + typeStr += "tournoi "; + + setConfirm({ + title: "Changement de type de catégorie", + message: `Voulez-vous vraiment enlever la partie ${typeStr} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !`, + confirm: () => { + applyChanges(); + } + }) + confirmRef.current.click(); + } else { + applyChanges(); + } + } else { + toast.promise(sendRequest('createCategory', {name: name.trim(), liceName: lice.trim(), type: nType}), + { + pending: 'Création de la catégorie...', + success: 'Catégorie créée !', + error: 'Erreur lors de la création de la catégorie' + } + ).then(id => { + setCatId(id); + + if (tournoi) { + const trees = build_tree(size, loserMatch) + console.log("Creating trees for new category:", trees); + + toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), + { + pending: 'Création des arbres du tournoi...', + success: 'Arbres créés !', + error: 'Erreur lors de la création des arbres' + } + ) + } + }) + } + + const data = { + name: name.trim(), + liceName: lice.trim(), + type: poule + (tournoi << 1), + size: size, + loserMatch: loserMatch + } + console.log("Submitting category data:", data); + } + + return
+
+

Ajouter une catégorie

+ +
+
+
+ + setName(e.target.value)}/> +
+ +
+ + setLice(e.target.value)}/> +
+ + +
+ +
+ setPoule(e.target.checked)}/> + +
+ +
+ setTournoi(e.target.checked)}/> + +
+
+ +
+ + setSize(Number.parseInt(e.target.value))}/> +
+ +
+ Match pour les perdants du tournoi: +
+ { + if (e.target.checked) setLoserMatch(-1) + }}/> + +
+
+ { + if (e.target.checked) setLoserMatch(1) + }}/> + +
+
+ { + if (e.target.checked) setLoserMatch(0) + }}/> + +
+
+ +
+
+ + +
+
+} + +function CategoryContent({cat, catId, setCat}) { + const setLoading = useLoadingSwitcher() + const {sendRequest, dispatch} = useWS(); + const [matches, reducer] = useReducer(MarchReducer, []); + + useEffect(() => { + const treeListener = ({data}) => { + if (!cat || data.length < 1 || data[0].categorie !== cat.id) + return + setCat({ + ...cat, + trees: data.map(d => from_sendTree(d, true)) + }) + let matches2 = []; + data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => matches2.push({...data_})); + reducer({type: 'REPLACE_TREE', payload: matches2}); + } + dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}}) + return () => dispatch({type: 'removeListener', payload: treeListener}) + }, [cat]); + + useEffect(() => { + if (!catId) + return; + setLoading(1); + sendRequest('getFullCategory', catId) + .then((data) => { + setCat({ + id: data.id, + name: data.name, + liceName: data.liceName, + type: data.type, + trees: data.trees.map(d => from_sendTree(d, true)) + }) + + let matches2 = []; + data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => matches2.push({...data_})); + data.matches.forEach((data_) => matches2.push({...data_})); + + reducer({type: 'REPLACE_ALL', payload: matches2}); + }).finally(() => setLoading(0)) + }, [catId]); + + console.log("Matches in category content:", matches); + + return <> +
+ + +
+
+
+
+ +} diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx index 1b2f7d4..2b76a9b 100644 --- a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx +++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx @@ -3,6 +3,7 @@ import {LoadingProvider} from "../../../hooks/useLoading.jsx"; import {useEffect, useState} from "react"; import {useWS, WSProvider} from "../../../hooks/useWS.jsx"; import {ColoredCircle} from "../../../components/ColoredCircle.jsx"; +import {CMAdmin} from "./CMAdmin.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -36,10 +37,13 @@ function HomeComp() { return - - }/> - }/> - + + + }/> + }/> + }/> + + } @@ -79,8 +83,8 @@ function Home2({perm}) {

Sélectionne les modes d'affichage

{perm === "admin" && <> - - + + } {perm === "table" && <> diff --git a/src/main/webapp/src/utils/MatchReducer.jsx b/src/main/webapp/src/utils/MatchReducer.jsx new file mode 100644 index 0000000..4884675 --- /dev/null +++ b/src/main/webapp/src/utils/MatchReducer.jsx @@ -0,0 +1,44 @@ +export function MarchReducer(datas, action) { + switch (action.type) { + case 'ADD': + return [ + ...datas, + action.payload + ] + case 'ADD_ALL': + return [ + ...datas, + ...action.payload + ] + case 'REPLACE_ALL': + return [ + ...action.payload + ] + case 'REPLACE_TREE': + return [ + ...datas.filter(data => data.categorie_ord !== -42), + ...action.payload + ] + case 'CLEAR': + return [] + case 'REMOVE': + return datas.filter(data => data.id !== action.payload) + case 'REMOVE_TREE': + return datas.filter(data => data.categorie_ord !== -42) + case 'UPDATE_OR_ADD': + const index = datas.findIndex(data => data.id === action.payload.id) + if (index === -1) { + return [ + ...datas, + action.payload + ] + } else { + datas[index] = action.payload + return [...datas] + } + case 'SORT': + return datas.sort(action.payload) + default: + throw new Error() + } +} diff --git a/src/main/webapp/src/utils/TreeUtils.js b/src/main/webapp/src/utils/TreeUtils.js new file mode 100644 index 0000000..d62c59c --- /dev/null +++ b/src/main/webapp/src/utils/TreeUtils.js @@ -0,0 +1,129 @@ +export function TreeNode(data, left = null, right = null) { + this.data = data; + this.left = left; + this.right = right; +} + +TreeNode.prototype = { + getMaxChildrenAtDepth: function (death, current = 0) { + if (current === death) + return 1; + + let tmp = 0; + if (this.right != null) + tmp += this.right.getMaxChildrenAtDepth(death, current + 1); + + if (this.left != null) + tmp += this.left.getMaxChildrenAtDepth(death, current + 1); + + return tmp; + }, + death: function () { + let dg = 0; + let dd = 0; + + if (this.right != null) + dg = this.right.death(); + + if (this.left != null) + dg = this.left.death(); + + return 1 + Math.max(dg, dd); + }, + isEnd: function (data) { + if (this.data === data) { + return this.right == null && this.left == null; + } else { + return (this.right != null && this.right.isEnd(data)) || (this.left != null && this.left.isEnd(data)); + } + }, + flat: function () { + let out = [] + this.__flat(out) + return out; + }, + __flat: function (out) { + out.push(this.data) + if (this.left != null) + this.left.__flat(out) + if (this.right != null) + this.right.__flat(out) + } +} + +export function build_tree(nb_comb, sub_tree_level = 0) { + const nb_match = Math.ceil(nb_comb / 2) + const level = Math.ceil(Math.log(nb_match) / Math.log(2)) + + const treeNodes = [] + + for (let i = level; i >= 0; i--) { + const treeNode = new TreeNode(null) + build_tree_(treeNode, 0, i, 0, nb_match) + treeNodes.push(treeNode) + + if (sub_tree_level !== -1) { + if (sub_tree_level === 0) + break + if (i > sub_tree_level) + i = sub_tree_level + } + } + + return treeNodes +} + +export function build_tree_(treeNode, death, death_target, leave, leave_target) { + if (death === death_target) { + return (leave < leave_target) ? 1 : 0; + } + + if (leave + 1 === leave_target) { + return 1; + } else { + let to_add = 0; + if (leave < leave_target) { + treeNode.left = new TreeNode(null) + to_add += build_tree_(treeNode.left, death + 1, death_target, leave, leave_target) + } + + if (leave + to_add < leave_target) { + treeNode.right = new TreeNode(null) + to_add += build_tree_(treeNode.right, death + 1, death_target, leave + to_add, leave_target) + } + + return to_add; + } +} + +export function resize_tree(src, to) { + if (src === null || to === null) + return null; + + if (src.left === null && to.left != null) { + src.left = to.left; + } else if (src.left != null && to.left === null) { + src.left = null; + } else { + resize_tree(src.left, to.left); + } + + if (src.right === null && to.right != null) { + src.right = to.right; + } else if (src.right != null && to.right === null) { + src.right = null; + } else { + resize_tree(src.right, to.right); + } +} + +export function from_sendTree(tree, matchId = true) { + if (tree == null) + return null; + + const node = new TreeNode(matchId ? tree.match?.id : tree.match); + node.left = from_sendTree(tree.left, matchId); + node.right = from_sendTree(tree.right, matchId); + + return node; +}