feat: categorie on CM

This commit is contained in:
Thibaut Valentin 2025-11-24 15:56:15 +01:00
parent b78b3f005b
commit b1bcf75e56
28 changed files with 1343 additions and 43 deletions

View File

@ -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;
}

View File

@ -42,4 +42,6 @@ public class CategoryModel {
List<TreeModel> tree;
Integer type;
String liceName = "1";
}

View File

@ -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;
}
}

View File

@ -55,6 +55,10 @@ public class CompetitionModel {
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<RegisterModel> insc;
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<CompetitionGuestModel> guests = new ArrayList<>();
List<Long> banMembre = new ArrayList<>();
String owner;

View File

@ -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<ScoreEmbeddable> scores = new ArrayList<>();
char poule = 'A';
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "match", referencedColumnName = "id")
List<CardboardModel> 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;
}

View File

@ -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<CategoryModel, Long> {
public Uni<CategoryModel> create(CategoryModel categoryModel) {
categoryModel.setSystem(CompetitionSystem.INTERNAL);
return Panache.withTransaction(() -> this.persist(categoryModel)
.invoke(categoryModel1 -> categoryModel1.setSystemId(categoryModel1.getId())))
.chain(this::persist);
}
}

View File

@ -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<CompetitionGuestModel, Long> {
}

View File

@ -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<MatchModel, Long> {
public Uni<MatchModel> create(MatchModel matchModel) {
matchModel.setSystem(CompetitionSystem.INTERNAL);
return Panache.withTransaction(() -> this.persistAndFlush(matchModel)
.invoke(matchModel1 -> matchModel1.setSystemId(matchModel1.getId())))
.chain(this::persist);
}
}

View File

@ -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<TreeModel, Long> {
@WithTransaction
public Uni<Boolean> deleteTree(TreeModel entity) {
Uni<Boolean> 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()));
}
}

View File

@ -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<CategoryData> 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<TreeModel> 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<Long, MembreModel> membres = new HashMap<>();
HashMap<String, CompetitionGuestModel> guest = new HashMap<>();
List<MatchModel> match = new ArrayList<>();
List<Long> toRmMatch;
List<TreeModel> 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())));
}
}

View File

@ -89,7 +89,7 @@ public class CompetPermService {
.onFailure().call(throwable -> cacheAccess.invalidate(securityCtx.getSubject()));
Uni<HashMap<Long, String>> none = cacheNoneAccess.getAsync(securityCtx.getSubject(),
k -> competitionRepository.list("system = ?1", CompetitionSystem.NONE)
k -> competitionRepository.list("system = ?1", CompetitionSystem.INTERNAL)
.map(competitionModels -> {
HashMap<Long, String> 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();

View File

@ -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());
}
}

View File

@ -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<MatchData> 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);

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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<ScoreEmbeddable> scores;
private char poule;
private List<CardboardEntity> 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;
}
}

View File

@ -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());
}
}

View File

@ -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());
}

View File

@ -1,5 +1,5 @@
package fr.titionfire.ffsaf.utils;
public enum CompetitionSystem {
SAFCA, NONE
SAFCA, INTERNAL
}

View File

@ -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<Method, Object> 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<Void> sendNotifyToOtherEditor(WebSocketConnection connection, String code, Object data) {
String uuid = connection.pathParam("uuid");
List<Uni<Void>> 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<Void> error(WebSocketConnection connection, ForbiddenException t) {
return connection.close(CloseReason.INTERNAL_SERVER_ERROR);

View File

@ -0,0 +1,8 @@
package fr.titionfire.ffsaf.ws;
public enum PermLevel {
NONE,
VIEW,
TABLE,
ADMIN,
}

View File

@ -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<List<JustCategorie>> 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<FullCategory> 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<Long> 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<Void> 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<Long> 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<TreeModel> 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<Void> updateNode(TreeNode<Long> 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<Void> 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<Long> 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<TreeNode<Long>> trees) {
}
@Data
@RegisterForReflection
public static class FullCategory {
long id;
String name;
int type;
String liceName;
List<TreeEntity> trees = null;
List<MatchEntity> matches;
}
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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 <>
<div className="card">
<div className='card-header'>
<CategoryHeader cat={cat} setCatId={setCatId}/>
</div>
<div className="card-body">
<LoadingProvider>
<div className="row">
<CategoryContent cat={cat} catId={catId} setCat={setCat}/>
</div>
</LoadingProvider>
</div>
</div>
</>
}
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 <div className="row">
<div className="col-auto">
<div className="input-group">
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5>
<select className="form-select" onChange={handleCatChange}>
{cats && cats.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))}
{cats && <option value={-1}>Nouvelle...</option>}
</select>
</div>
</div>
<div className="col">
</div>
<div className="col">
<button className="btn btn-primary float-end" onClick={() => {
setModal(cat);
bthRef.current.click();
}} disabled={cat === null}>Modifier
</button>
</div>
<button ref={bthRef} data-bs-toggle="modal" data-bs-target="#CategorieModal" style={{display: "none"}}>open</button>
<div className="modal fade" id="CategorieModal" tabIndex="-1" aria-labelledby="CategorieModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<ModalContent state={modal} setCatId={setCatId} setConfirm={setConfirm} confirmRef={confirmRef}/>
</div>
</div>
</div>
<button ref={confirmRef} data-bs-toggle="modal" data-bs-target="#confirm-dialog" style={{display: "none"}}>open</button>
<ConfirmDialog id="confirm-dialog" onConfirm={confirm.confirm ? confirm.confirm : () => {
}} onCancel={confirm.cancel ? confirm.cancel : () => {
}} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/>
</div>
}
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 <form onSubmit={handleSubmit}>
<div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">Ajouter une catégorie</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="mb-3">
<label htmlFor="nameInput1" className="form-label">Nom</label>
<input type="text" className="form-control" id="nameInput1" placeholder="Epée bouclier" name="name" value={name}
onChange={e => setName(e.target.value)}/>
</div>
<div className="mb-3">
<label htmlFor="liceInput1" className="form-label">Nom des lices <small>(séparée par des ';')</small></label>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="lice" value={lice}
onChange={e => setLice(e.target.value)}/>
</div>
<div className="mb-3">
<label className="form-label">Type</label>
<div className="form-check form-switch">
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault" name="poule" checked={poule}
onChange={e => setPoule(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckDefault">Poule</label>
</div>
<div className="form-check form-switch">
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault2" name="trournoi" checked={tournoi}
onChange={e => setTournoi(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckDefault2">Tournoi</label>
</div>
</div>
<div className="mb-3">
<label htmlFor="sizeInput1" className="form-label">Nombre de combattants</label>
<input type="number" className="form-control" id="sizeInput1" placeholder="4" name="size" disabled={!tournoi} value={size}
onChange={e => setSize(Number.parseInt(e.target.value))}/>
</div>
<div className="mb-3">
<span>Match pour les perdants du tournoi:</span>
<div className="form-check">
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault1" disabled={!tournoi}
checked={loserMatch === -1} onChange={e => {
if (e.target.checked) setLoserMatch(-1)
}}/>
<label className="form-check-label" htmlFor="radioDefault1">
Tous les matchs
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault2" disabled={!tournoi}
checked={loserMatch === 1} onChange={e => {
if (e.target.checked) setLoserMatch(1)
}}/>
<label className="form-check-label" htmlFor="radioDefault2">
Demi-finales et finales
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault3" disabled={!tournoi}
checked={loserMatch === 0} onChange={e => {
if (e.target.checked) setLoserMatch(0)
}}/>
<label className="form-check-label" htmlFor="radioDefault3">
Finales uniquement
</label>
</div>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
</div>
</form>
}
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 <>
<div className="col">
<div className="vr"></div>
</div>
<div className="col">
</div>
</>
}

View File

@ -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 <WSProvider url={`${vite_url.replace('http', 'ws')}/api/ws/competition/${compUuid}`} onmessage={messageHandler}>
<WSStatus setPerm={setPerm}/>
<Routes>
<Route path="/" element={<Home2 perm={perm}/>}/>
<Route path="/test" element={<Test2/>}/>
</Routes>
<LoadingProvider>
<Routes>
<Route path="/" element={<Home2 perm={perm}/>}/>
<Route path="/admin" element={<CMAdmin/>}/>
<Route path="/test" element={<Test2/>}/>
</Routes>
</LoadingProvider>
</WSProvider>
}
@ -79,8 +83,8 @@ function Home2({perm}) {
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4>
<div className="col">
{perm === "admin" && <>
<button className="btn btn-primary" onClick={() => nav("admin")}>Administration</button>
<button className="btn btn-primary ms-3" onClick={() => nav("table")}>Table de marque</button>
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button>
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button>
</>}
{perm === "table" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button>

View File

@ -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()
}
}

View File

@ -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;
}