From bf377c9c5e273e9e70a8518d382810d3244e190e Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Mon, 29 Dec 2025 21:03:05 +0100 Subject: [PATCH] feat: external website result connector --- .../ffsaf/data/model/RegisterModel.java | 9 + .../ffsaf/domain/service/ResultService.java | 485 ++++++--- .../ffsaf/domain/service/UpdateService.java | 23 + .../ffsaf/rest/ExternalResultEndpoints.java | 85 ++ .../ffsaf/rest/data/ResultCategoryData.java | 2 + .../java/fr/titionfire/ffsaf/utils/Utils.java | 14 + src/main/webapp/.gitignore | 1 + src/main/webapp/public/competition.js | 946 ++++++++++++++++++ .../src/pages/competition/editor/CMAdmin.jsx | 26 +- .../editor/CompetitionManagerRoot.jsx | 2 +- 10 files changed, 1440 insertions(+), 153 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java create mode 100644 src/main/webapp/public/competition.js diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 02c0f90..16691d3 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -66,4 +66,13 @@ public class RegisterModel { return membre.club; return club; } + + public Categorie getCategorie2() { + Categorie tmp = this.categorie; + if (tmp == null) + tmp = membre.getCategorie(); + if (tmp == null) + return null; + return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)]; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index ecd48a4..e83009c 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -1,10 +1,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.*; -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.RegisterRepository; +import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.rest.data.ResultCategoryData; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.utils.*; @@ -19,7 +16,9 @@ import lombok.Builder; import org.hibernate.reactive.mutiny.Mutiny; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; @WithSession @@ -35,6 +34,12 @@ public class ResultService { @Inject MembreService membreService; + @Inject + ClubRepository clubRepository; + + @Inject + CompetitionGuestRepository competitionGuestRepository; + @Inject CategoryRepository categoryRepository; @@ -53,10 +58,22 @@ public class ResultService { .collect().asList(); } + public Uni> getCategoryList(String uuid) { + return categoryRepository.list("compet.uuid = ?1", uuid) + .map(categoryModels -> { + HashMap map = new HashMap<>(); + categoryModels.stream() + .sorted(Comparator.comparing(CategoryModel::getName)) + .forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId())); + return map; + }); + } + public Uni> getCategory(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) .chain(m -> categoryRepository.list("compet.uuid = ?1", uuid) - .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True + .chain(cats -> matchRepository.list( + "(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True m.getMembre(), cats))) .map(matchModels -> { HashMap> map = new HashMap<>(); @@ -82,6 +99,8 @@ public class ResultService { CategoryModel categoryModel = matchModels.get(0).getCategory(); out.setName(categoryModel.getName()); out.setType(categoryModel.getType()); + out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";")); + out.setGenTime(System.currentTimeMillis()); getArray2(matchModels, out); getTree(categoryModel.getTree(), out); @@ -173,6 +192,12 @@ public class ResultService { } } + public Uni getCategoryPublic(String uuid, long poule) { + return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule) + .call(list -> Mutiny.fetch(list.get(0).getCategory().getTree())) + .map(this::getData); + } + private void getTree(List treeModels, ResultCategoryData out) { ArrayList> trees = new ArrayList<>(); treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> { @@ -185,87 +210,230 @@ public class ResultService { public Uni getAllCombArray(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) - .chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) - .map(pair -> { - List registers = pair.getKey(); - List matchModels = pair.getValue(); + .chain(__ -> getAllCombArray(uuid)); + } - CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); + public Uni getAllCombArrayPublic(String uuid) { + return getAllCombArray(uuid); + } - List combs = matchModels.stream() - .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) - .filter(Objects::nonNull) - .distinct() - .map(combName -> { - var builder2 = CombsArrayData.CombsData.builder(); - AtomicInteger w = new AtomicInteger(0); - AtomicInteger l = new AtomicInteger(0); - AtomicInteger pointMake = new AtomicInteger(); - AtomicInteger pointTake = new AtomicInteger(); + public Uni getAllCombArray(String uuid) { + return registerRepository.list("competition.uuid = ?1", uuid) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); - matchModels.stream() - .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) - || m.getC2Name().equals(combName))) - .forEach(matchModel -> { - int win = matchModel.win(); - if ((combName.equals(matchModel.getC1Name()) && win > 0) || - combName.equals(matchModel.getC2Name()) && win < 0) { - w.getAndIncrement(); - } else { - l.getAndIncrement(); - } + CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) - .forEach(score -> { - if (combName.equals(matchModel.getC1Name())) { - pointMake.addAndGet(score.getS1()); - pointTake.addAndGet(score.getS2()); - } else { - pointMake.addAndGet(score.getS2()); - pointTake.addAndGet(score.getS1()); - } - }); - }); + List combs = matchModels.stream() + .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) + .filter(Objects::nonNull) + .distinct() + .map(combName -> { + var builder2 = CombsArrayData.CombsData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); - Categorie categorie = null; - ClubModel club = null; + matchModels.stream() + .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) + || m.getC2Name().equals(combName))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((combName.equals(matchModel.getC1Name()) && win > 0) || + combName.equals(matchModel.getC2Name()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } - Optional register = registers.stream() - .filter(r -> r.getName().equals(combName)).findFirst(); - if (register.isPresent()) { - categorie = register.get().getCategorie(); - club = register.get().getClub2(); - } + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (combName.equals(matchModel.getC1Name())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); - builder2.name(combName); - builder2.w(w.get()); - builder2.l(l.get()); - builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); - builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); - builder2.pointMake(pointMake.get()); - builder2.pointTake(pointTake.get()); - builder2.ratioPoint( - (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + Categorie categorie = null; + ClubModel club = null; - return builder2.build(); - }) - .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) - .toList(); + Optional register = registers.stream() + .filter(r -> r.getName().equals(combName)).findFirst(); + if (register.isPresent()) { + categorie = register.get().getCategorie(); + club = register.get().getClub2(); + } - builder.nb_insc(combs.size()); - builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); - builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); - builder.combs(combs); + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(combName); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); - return builder.build(); + return builder2.build(); + }) + .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) + .toList(); + + builder.nb_insc(combs.size()); + builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); + builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); + builder.combs(combs); + + return builder.build(); + }); + } + + public Uni> getCombList(String uuid) { + return registerRepository.list("competition.uuid = ?1", uuid) + .map(models -> { + HashMap map = new HashMap<>(); + models.forEach(registerEmbeddable -> { + map.put(Utils.getFullName(registerEmbeddable.getMembre()), + registerEmbeddable.getMembre().getFname() + "¤" + registerEmbeddable.getMembre() + .getLname()); + }); + return map; + }) + .chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid) + .map(models -> { + models.forEach(guestModel -> map.put(Utils.getFullName(guestModel), + guestModel.getFname() + "¤" + guestModel.getLname())); + return map; }) ); } + public Uni getCombArrayPublic(String uuid, String fname, String lname) { + CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder(); + AtomicBoolean guest = new AtomicBoolean(false); + AtomicLong id = new AtomicLong(0); + + + return registerRepository.find("membre.fname = ?1 AND membre.lname = ?2 AND competition.uuid = ?3", fname, + lname, uuid).firstResult() + .chain(registerModel -> { + if (registerModel == null) { + return competitionGuestRepository.find("fname = ?1 AND lname = ?2 AND competition.uuid = ?3", + fname, lname, uuid).firstResult() + .chain(guestModel -> { + builder.name(Utils.getFullName(guestModel)); + builder.club(guestModel.getClub()); + guest.set(true); + id.set(guestModel.getId()); + builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie() + .getName(BUNDLE)); + + return matchRepository.list( + "category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)", uuid, + guestModel); + }); + } + builder.name(Utils.getFullName(registerModel.getMembre())); + builder.club((registerModel.getClub2() == null) ? BUNDLE.getString( + "no.licence") : registerModel.getClub2().getName()); + id.set(registerModel.getMembre().getId()); + builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2() + .getName(BUNDLE)); + + return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid, + registerModel.getMembre()); + }) + .invoke(matchModels -> { + List pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct() + .toList(); + List matchs = new ArrayList<>(); + + AtomicInteger sumW = new AtomicInteger(); + AtomicInteger sumPointMake = new AtomicInteger(0); + AtomicInteger sumPointTake = new AtomicInteger(0); + + for (MatchModel matchModel : matchModels) { + if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) || + (matchModel.getC2_id() == null && matchModel.getC2_guest() == null)) + continue; + + var builder2 = CombArrayData.MatchsData.builder(); + builder2.date(matchModel.getDate()); + builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory())) + .map(CategoryModel::getName).findFirst().orElse("")); + + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + if ((!guest.get() && matchModel.getC1_id() != null && matchModel.getC1_id().getId() == id.get()) + || (guest.get() && matchModel.getC1_guest() != null && matchModel.getC1_guest() + .getId() == id.get())) { + builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest())); + if (matchModel.isEnd()) { + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(scoreEntity -> { + pointMake.addAndGet(scoreEntity.getS1()); + pointTake.addAndGet(scoreEntity.getS2()); + }); + builder2.score(matchModel.getScores().stream() + .map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()); + } else { + builder2.score(new ArrayList<>()); + } + builder2.win(matchModel.win() > 0); + } else { + builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest())); + if (matchModel.isEnd()) { + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(scoreEntity -> { + pointMake.addAndGet(scoreEntity.getS2()); + pointTake.addAndGet(scoreEntity.getS1()); + }); + builder2.score(matchModel.getScores().stream() + .map(s -> new Integer[]{s.getS2(), s.getS1()}).toList()); + } else { + builder2.score(new ArrayList<>()); + } + builder2.win(matchModel.win() < 0); + } + + builder2.ratio( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + + sumPointMake.addAndGet(pointMake.get()); + sumPointTake.addAndGet(pointTake.get()); + + matchs.add(builder2.build()); + if (builder2.win) + sumW.getAndIncrement(); + } + + builder.totalWin(sumW.get()); + builder.pointRatio( + (sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get()); + builder.pointMake(sumPointMake.get()); + builder.pointTake(sumPointTake.get()); + + matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule) + .thenComparing(CombArrayData.MatchsData::adv)); + + builder.matchs(matchs); + }) + .map(__ -> builder.build()); + } + @Builder @RegisterForReflection @@ -277,96 +445,117 @@ public class ResultService { } } + @Builder + @RegisterForReflection + public static record CombArrayData(String name, String club, String cat, int totalWin, + float pointRatio, int pointMake, int pointTake, List matchs) { + @Builder + @RegisterForReflection + public static record MatchsData(Date date, String poule, String adv, List score, float ratio, + boolean win) { + } + } + + public Uni> getClubList(String uuid) { // TODO add guest club + return registerRepository.list("competition.uuid = ?1", uuid) + .map(registers -> { + HashMap registerMap = new HashMap<>(); + registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull) + .forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId())); + return registerMap; + }); + } + public Uni getClubArray(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx).chain(cm_register -> getClubArray(uuid, cm_register.getClub2())); + } + + public Uni getClubArrayPublic(String uuid, Long id) { + return clubRepository.findById(id).chain(clubModel -> getClubArray(uuid, clubModel)); + } + + public Uni getClubArray(String uuid, ClubModel clubModel) { ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder(); + builder.name(clubModel.getName()); - return hasAccess(uuid, securityCtx) - .invoke(cm_register -> builder.name(cm_register.getClub2().getName())) - .chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, - cm_register.getClub2()) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) - .map(pair -> { - List registers = pair.getKey(); - List matchModels = pair.getValue(); + return registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); - builder.nb_insc(registers.size()); + builder.nb_insc(registers.size()); - AtomicInteger tt_win = new AtomicInteger(0); - AtomicInteger tt_match = new AtomicInteger(0); + AtomicInteger tt_win = new AtomicInteger(0); + AtomicInteger tt_match = new AtomicInteger(0); - List combData = registers.stream() - .map(register -> { + List combData = registers.stream().map(register -> { + var builder2 = ClubArrayData.CombData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); - var builder2 = ClubArrayData.CombData.builder(); - AtomicInteger w = new AtomicInteger(0); - AtomicInteger l = new AtomicInteger(0); - AtomicInteger pointMake = new AtomicInteger(); - AtomicInteger pointTake = new AtomicInteger(); + matchModels.stream() + .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) + || register.getMembre().equals(m.getC2_id()))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((register.getMembre().equals(matchModel.getC1_id()) && win > 0) || + register.getMembre().equals(matchModel.getC2_id()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } - matchModels.stream() - .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) - || register.getMembre().equals(m.getC2_id()))) - .forEach(matchModel -> { - int win = matchModel.win(); - if ((register.getMembre() - .equals(matchModel.getC1_id()) && win > 0) || - register.getMembre() - .equals(matchModel.getC2_id()) && win < 0) { - w.getAndIncrement(); - } else { - l.getAndIncrement(); - } + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (register.getMembre().equals(matchModel.getC1_id())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) - .forEach(score -> { - if (register.getMembre() - .equals(matchModel.getC1_id())) { - pointMake.addAndGet(score.getS1()); - pointTake.addAndGet(score.getS2()); - } else { - pointMake.addAndGet(score.getS2()); - pointTake.addAndGet(score.getS1()); - } - }); - }); + Categorie categorie = register.getCategorie(); + if (categorie == null) + categorie = register.getMembre().getCategorie(); - Categorie categorie = register.getCategorie(); - if (categorie == null) - categorie = register.getMembre().getCategorie(); + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(register.getName()); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); - builder2.name(register.getName()); - builder2.w(w.get()); - builder2.l(l.get()); - builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); - builder2.pointMake(pointMake.get()); - builder2.pointTake(pointTake.get()); - builder2.ratioPoint( - (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + tt_win.addAndGet(w.get()); + tt_match.addAndGet(w.get() + l.get()); - tt_win.addAndGet(w.get()); - tt_match.addAndGet(w.get() + l.get()); + return builder2.build(); + }) + .sorted(Comparator.comparing(ClubArrayData.CombData::name)) + .toList(); - return builder2.build(); - }) - .sorted(Comparator.comparing(ClubArrayData.CombData::name)) - .toList(); + builder.nb_match(tt_match.get()); + builder.match_w(tt_win.get()); + builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); + builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); + builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); + builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); + builder.combs(combData); - builder.nb_match(tt_match.get()); - builder.match_w(tt_win.get()); - builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) - .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); - builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); - builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); - builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) - .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); - builder.combs(combData); - - return builder.build(); - }) - ); + return builder.build(); + }); } @Builder diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java new file mode 100644 index 0000000..c04c643 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java @@ -0,0 +1,23 @@ +package fr.titionfire.ffsaf.domain.service; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.HashMap; + +@ApplicationScoped +public class UpdateService { // For public result page + static HashMap lastUpdate = new HashMap<>(); + + public void setNewData(long id) { + lastUpdate.put(id, System.currentTimeMillis()); + } + + public boolean needUpdate(long id, long last_update) { + if (!lastUpdate.containsKey(id)) { + lastUpdate.put(id, System.currentTimeMillis() - 5000); + return true; + } + + return lastUpdate.getOrDefault(id, 0L) > last_update; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java new file mode 100644 index 0000000..aee3914 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java @@ -0,0 +1,85 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.ResultService; +import fr.titionfire.ffsaf.domain.service.UpdateService; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.HashMap; + +@Path("api/public/result/{id}") +public class ExternalResultEndpoints { + + @Inject + ResultService resultService; + + @Inject + UpdateService updateService; + + @PathParam("id") + private String id; + + @GET + @Path("/poule/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> list() { + return resultService.getCategoryList(id); + } + + @GET + @Path("/poule/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getArray(@QueryParam("poule") long poule, @DefaultValue("-1") @QueryParam("rf") long rf) { + if (poule == 0) + return Uni.createFrom().voidItem(); + + if (updateService.needUpdate(poule, rf)) { + return resultService.getCategoryPublic(id, poule); + } else { + return Uni.createFrom().voidItem(); + } + } + + @GET + @Path("/comb/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> combList() { + return resultService.getCombList(id); + } + + @GET + @Path("/comb/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getArray(@QueryParam("comb") String comb) { + if (comb.equals("0")) + return Uni.createFrom().item(""); + return resultService.getCombArrayPublic(id, comb.substring(0, comb.indexOf('¤')), comb.substring(comb.indexOf('¤') + 1)); + } + + @GET + @Path("/comb/get_all") + @Produces(MediaType.APPLICATION_JSON) + public Uni getAll() { + return resultService.getAllCombArrayPublic(id); + } + + + + @GET + @Path("/club/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> clubList() { + return resultService.getClubList(id); + } + + @GET + @Path("/club/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getClubArray(@QueryParam("club") long club) { + if (club == 0) + return Uni.createFrom().item(""); + return resultService.getClubArrayPublic(id, club); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java index 2054e52..3ff949f 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -21,6 +21,8 @@ public class ResultCategoryData { HashMap> matchs = new HashMap<>(); HashMap> rankArray = new HashMap<>(); ArrayList> trees; + String[] liceName; + long genTime; @Data @AllArgsConstructor diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index b541a2e..62e720b 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -1,5 +1,7 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; +import fr.titionfire.ffsaf.data.model.MembreModel; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; @@ -365,4 +367,16 @@ public class Utils { } return result.toString().trim(); } + + public static String getFullName(Object ...models) { + for (Object model : models){ + if (model == null) + continue; + if (model instanceof MembreModel membreModel) + return membreModel.getFname() + " " + membreModel.getLname(); + if (model instanceof CompetitionGuestModel guestModel) + return guestModel.getFname() + " " + guestModel.getLname(); + } + return ""; + } } diff --git a/src/main/webapp/.gitignore b/src/main/webapp/.gitignore index a547bf3..a461aa8 100644 --- a/src/main/webapp/.gitignore +++ b/src/main/webapp/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +/public/result_test.html diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js new file mode 100644 index 0000000..53ddd7b --- /dev/null +++ b/src/main/webapp/public/competition.js @@ -0,0 +1,946 @@ +let apiUrlRoot = ""; +const rootDiv = document.getElementById("safca_api_data"); + +const header = `

Résultat de la compétition :

` +const backButton = `Retour` +const cupImg = `` + +const voidFunction = () => { +} +let lastRf = 0; +let rfFonction = voidFunction; + +setInterval(() => { + rfFonction(); +}, 15000); + +function setSubPage(name) { + window.location.hash = name; + const location = name.split('/'); + console.log(location); + + switch (location[0]) { + case 'home': + homePage(); + break; + case 'poule': + poulePage(location); + break; + case 'comb': + combPage(location); + break; + case 'club': + clubPage(location); + break; + case 'all': + combsPage(); + break; + } +} + +function homePage() { + rootDiv.innerHTML = header; + + let content = document.createElement('div'); + content.innerHTML = ` + + ` + rootDiv.append(content) +} + +let loadingAnimationStep = 0; + +function startLoading(root) { + const id = "loading" + Math.random().toString(36); + let element = document.createElement('h2'); + element.id = id; + + const anim = setInterval(() => { + let str = "Chargement"; + for (let i = 0; i < loadingAnimationStep; i++) { + str += "."; + } + loadingAnimationStep = (loadingAnimationStep + 1) % 4; + element.innerText = str; + }, 500); + + root.append(element) + return {interval: anim, root: root, element: element}; +} + +function stopLoading(loading) { + clearInterval(loading['interval']); + loading['root'].removeChild(loading['element']); +} + +function scoreToString(score) { + const scorePrint = (s1) => { + switch (s1) { + case -997: + return "disc."; + case -998: + return "abs."; + case -999: + return "for."; + case -1000: + return ""; + default: + return String(s1); + } + } + + return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); +} + +function dateToString(date) { + if (date === null || date === undefined) + return ""; + + const date_ = new Date(date); + const date_2 = new Date(date); + const current = new Date(); + current.setHours(0, 0, 0, 0); + date_2.setHours(0, 0, 0, 0); + + let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24)); + if (d === 0) + return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else if (d === 1) + return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else if (d === 2) + return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else + return date_.toLocaleDateString(); +} + +function buildPouleMenu(isPoule, change_view) { + const menuDiv = document.createElement('div'); + menuDiv.id = 'menu'; + menuDiv.style.borderBottom = '1px solid #9EA0A1'; + menuDiv.style.paddingBottom = '25px'; + + const ul = document.createElement('ul'); + ul.id = 'onglets'; + ul.style.position = 'absolute'; + ul.style.border = '1px solid transparent'; + ul.style.padding = '0'; + ul.style.font = 'bold 11px Batang, arial, serif'; + ul.style.listStyleType = 'none'; + ul.style.left = '50%'; + ul.style.marginTop = '0'; + ul.style.width = '430px'; + ul.style.marginLeft = '-215px'; + + function createTab(text, isActive, onClickHandler) { + const li = document.createElement('li'); + if (isActive) { + li.className = 'active'; + li.style.borderBottom = '1px solid #fff'; + li.style.backgroundColor = '#fff'; + } else { + li.style.backgroundColor = '#F4F9FD'; + } + li.style.float = 'left'; + li.style.height = '21px'; + li.style.margin = '2px 2px 0 2px !important'; + li.style.margin = '1px 2px 0 2px'; + li.style.border = '1px solid #9EA0A1'; + + const a = document.createElement('a'); + a.href = 'javascript:void(0);'; + a.onclick = onClickHandler; + a.textContent = text; + a.style.display = 'block'; + a.style.color = '#666'; + a.style.textDecoration = 'none'; + a.style.padding = '4px'; + + a.addEventListener('mouseover', function () { + a.style.background = '#fff'; + }); + + a.addEventListener('mouseout', function () { + if (!isActive) + a.style.background = '#F4F9FD'; + }); + + li.appendChild(a); + return li; + } + + const li1 = createTab('Poule', isPoule, function () { + change_view(true); + }); + ul.appendChild(li1); + const li2 = createTab('Tournois', !isPoule, function () { + change_view(false); + }); + ul.appendChild(li2); + + menuDiv.appendChild(ul); + + return menuDiv; +} + +function buildMatchArray(matchs) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +
+ + + + + + + + + + ` + for (const match of matchs) { + arrayContent += ` + + + + + + + + ` + } + arrayContent += `
RougeScoresBleuDate
${match.red}${match.red_w ? cupImg : ""}${scoreToString(match.score)}${match.blue_w ? cupImg : ""}${match.blue}${dateToString((match.red_w || match.blue_w) ? match.date : null)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function buildRankArray(rankArray) { + const arrayDiv = document.createElement('div'); + let arrayContent = `
+ + + + + + + + + + + ` + for (const row of rankArray) { + arrayContent += ` + + + + + + + + ` + + } + arrayContent += `
PlaceNomVictoireRatioPoints marquésPoints reçus
${row.rank}${row.name}${row.win}${row.pointRate.toFixed(3)}${row.pointMake}${row.pointTake}
` + arrayDiv.innerHTML = arrayContent; + return arrayDiv; +} + +function buildTree(treeData) { + return drawGraph(initTree(treeData)) +} + +function poulePage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par poule

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + let currentPoule = 0; + const loadPoule = () => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/poule/data?poule=${currentPoule}&rf=${lastRf}`) + .then(response => { + if (response.status === 204) // No content => no update + return; + + response.json().then(poule => { + lastRf = (poule.genTime !== undefined) ? poule.genTime : 0; + // console.log(poule); + + if (location.length === 3 && poule.type < 3) { + location.pop(); + window.location.hash = location.join('/'); + } + + dataContainer.replaceChildren(); + + if (poule.type === 1) { + for (const g in poule.matchs) { + if (Object.keys(poule.matchs).length > 1) { + const text = document.createElement('h4'); + text.textContent = `Groupe ${g}`; + text.style.marginTop = '2em'; + dataContainer.append(text); + } + dataContainer.append(buildMatchArray(poule.matchs[g])); + dataContainer.append(buildRankArray(poule.rankArray[g])); + } + } else if (poule.type === 2) { + dataContainer.append(buildTree(poule['trees'])); + } else { + const change_view = (isPoule) => { + dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view)); + + if (isPoule) { + for (const g in poule.matchs) { + if (Object.keys(poule.matchs).length > 1) { + const text = document.createElement('h4'); + text.textContent = `Groupe ${g}`; + text.style.marginTop = '2em'; + dataContainer.append(text); + } + dataContainer.append(buildMatchArray(poule.matchs[g])); + dataContainer.append(buildRankArray(poule.rankArray[g])); + } + } else { + dataContainer.append(buildTree(poule['trees'])); + } + + location[2] = isPoule ? 1 : 2; + window.location.hash = location.join('/'); + } + change_view((location.length === 3) ? location[2] === '1' : true); + } + }) + }) + .catch(e => { + console.error(e); + dataContainer.replaceChildren(new Text("Erreur de chargement de la poule")); + }) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/poule/list`) + .then(response => response.json()) + .then(poule => { + const select = document.createElement('select'); + select.setAttribute('id', poule.id); + select.innerHTML = ``; + for (const pouleKey of Object.keys(poule).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + location[1] = Object.keys(poule).find(key => poule[key] === Number(e.target.value)); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + lastRf = 0; + currentPoule = e.target.value; + loadPoule(); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = poule[decodeURI(location[1])]; + select.value = tmp; + currentPoule = tmp + loadPoule(); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des poules"))) + .finally(() => stopLoading(loading)); + + rfFonction = () => { + if (currentPoule !== 0) + loadPoule(); + } + + rootDiv.append(content) +} + +function buildCombView(comb) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Info :

+
    +
  • Nom Prénom : ${comb.name}
  • +
  • Club : ${comb.club}
  • +
  • Catégorie : ${comb.cat}
  • +
+

Statistique :

+
    +
  • Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})
  • +
  • Points marqués : ${comb.pointMake}
  • +
  • Points reçus : ${comb.pointTake}
  • +
  • Ratio du score (point marqué / point reçu): ${comb.pointRatio.toFixed(3)}
  • +
+ +

Liste des matchs:

+ +
+ + + + + + + + + + ` + for (const match of comb.matchs) { + arrayContent += ` + + + + + + + + ` + } + arrayContent += `
DatePouleAdversaireScoresRatio
${dateToString(match.date)}${match.poule}${match.adv}${scoreToString(match.score)}${match.ratio.toFixed(3)}${match.win ? cupImg : ""}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function combPage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par combattant

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loadComb = (id) => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/data?comb=${id}`) + .then(response => response.json()) + .then(comb => { + console.log(comb); + dataContainer.replaceChildren(buildCombView(comb)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant"))) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/list`) + .then(response => response.json()) + .then(combs => { + const select = document.createElement('select'); + select.innerHTML = ``; + for (const comb of Object.keys(combs).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + location[1] = Object.keys(combs).find(key => combs[key] === e.target.value); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + loadComb(e.target.value); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = combs[decodeURI(location[1])]; + select.value = tmp; + loadComb(tmp); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des combattants"))) + .finally(() => stopLoading(loading)); + + rootDiv.append(content) +} + +function buildClubView(club) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Info :

+
    +
  • Nom : ${club.name}
  • +
  • Nombre d'inscris : ${club.nb_insc}
  • +
+

Statistique :

+
    +
  • Nombre de match disputé : ${club.nb_match}
  • +
  • Nombre de victoires : ${club.match_w}
  • +
  • Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}
  • +
  • Points marqués : ${club.pointMake}
  • +
  • Points reçus : ${club.pointTake}
  • +
  • Ratio de points moyen : ${club.ratioPoint.toFixed(3)}
  • +
+ +

Liste des menbres :

+ +
+ + + + + + + + + + + + ` + for (const comb of club.combs) { + arrayContent += ` + + + + + + + + + + ` + } + arrayContent += `
CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points
${comb.cat}${comb.name}${comb.w}${comb.l}${comb.ratioVictoire.toFixed(3)}${comb.pointMake}${comb.pointTake}${comb.ratioPoint.toFixed(3)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function clubPage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par club

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loadComb = (id) => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/club/data?club=${id}`) + .then(response => response.json()) + .then(club => { + console.log(club); + dataContainer.replaceChildren(buildClubView(club)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club"))) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/club/list`) + .then(response => response.json()) + .then(clubs => { + const select = document.createElement('select'); + select.innerHTML = ``; + for (const club of Object.keys(clubs).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + if (e.target.value === '0') + return; + location[1] = Object.keys(clubs).find(key => clubs[key] === Number(e.target.value)); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + loadComb(e.target.value); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = clubs[decodeURI(location[1])]; + select.value = tmp; + loadComb(tmp); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des clubs"))) + .finally(() => stopLoading(loading)); + + rootDiv.append(content) +} + +function buildCombsView(combs) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Statistique :

+
    +
  • Nombre d'inscris : ${combs.nb_insc}
  • +
  • Nombre de match disputé : ${combs.tt_match}
  • +
  • Points marqués : ${combs.point}
  • +
+ +

Liste des combattants :

+ +
+ + + + + + + + + + + + + ` + for (const comb of combs.combs) { + arrayContent += ` + + + + + + + + + + + ` + } + arrayContent += `
CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points
${comb.cat}${comb.club}${comb.name}${comb.w}${comb.l}${comb.ratioVictoire.toFixed(3)}${comb.pointMake}${comb.pointTake}${comb.ratioPoint.toFixed(3)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function combsPage() { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/get_all`) + .then(response => response.json()) + .then(combs => { + console.log(combs); + dataContainer.replaceChildren(buildCombsView(combs)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste"))) + .finally(() => stopLoading(loading)); + + content.append(dataContainer); + rootDiv.append(content) +} + +window.addEventListener("load", () => { + let path = document.getElementById('safca_api_script').src; + const urlParams = new URLSearchParams(new URL(path).search); + apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id"); + + console.log("apiUrlRoot:", apiUrlRoot) + + let hash = window.location.hash.substring(1); + if (hash.length === 0) + setSubPage('home'); + else + setSubPage(hash); +}); + +class TreeNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + } + + death() { + 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); + } +} + +function initTree(data_in) { + out = []; + for (const din of data_in) { + out.push(parseTree(din)); + } + + return out; +} + +function parseTree(data_in) { + if (data_in?.data == null) + return null; + + let node = new TreeNode(data_in.data); + node.left = parseTree(data_in?.left); + node.right = parseTree(data_in?.right); + + return node; +} + +const max_x = 500; +const size = 24; + +function getBounds(root) { + let px = max_x; + let py; + let maxx, minx, miny, maxy + + function drawNode(tree, px, py) { + let death = tree.death() - 1 + + if (death === 0) { + if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0; + if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0; + } else { + if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0)) + miny = py - size * 2 * death - ((size * 1.5 / 2) | 0); + if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8; + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + maxx = px; + minx = px; + miny = py - (size * 1.5 / 2) | 0; + maxy = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + px = px - size * 2 - size * 8; + if (minx > px) minx = px; + + drawNode(node, px, py); + //graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny); + py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = maxx; + } + } else { + minx = 0; + maxx = 0; + miny = 0; + maxy = 0; + } + + return [minx, maxx, miny, maxy]; +} + +function drawGraph(root = []) { + const canvas = document.createElement('canvas'); + canvas.id = "myCanvas"; + canvas.style.border = "1px solid grey"; + canvas.style.marginTop = "10px"; + + const ctx = canvas.getContext("2d"); + + const [minx, maxx, miny, maxy] = getBounds(root); + canvas.width = maxx - minx; + canvas.height = maxy - miny; + ctx.translate(-minx, -miny); + + + ctx.fillStyle = "#000000" + ctx.lineWidth = 2 + ctx.strokeStyle = "#000000" + + function printText(s, x, y, width, height, lineG, lineD) { + ctx.save(); + ctx.translate(x, y); + + let tSize = 17; + let ratioX = height * 1. / 20.; + + ctx.font = "100 " + tSize + "px Arial"; + + let mw = width - (ratioX * 2) | 0; + if (ctx.measureText(s).width > mw) { + let dTextSize = true; + do { + tSize--; + ctx.font = tSize + "px Arial"; + } while (ctx.measureText(s).width > mw && tSize > 10) + + if (!dTextSize || ctx.measureText(s).width > mw) { + let s = ""; + for (const string2 in s.split(" ")) { + if (ctx.measureText(s + string2).width >= mw) { + s += "..."; + break; + } else { + s += string2 + " " + } + } + } + } + + const text = ctx.measureText(s) + let dx = (width - text.width) / 2; + let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillText(s, dx, dy, width - dy); + ctx.restore(); + + ctx.beginPath(); + if (lineD) { + ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2); + ctx.lineTo(x + width, y + height / 2) + } + if (lineG) { + ctx.moveTo(x, y + height / 2); + ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2) + } + ctx.stroke(); + } + + function printScores(scores, px, py, scale) { + + ctx.save(); + ctx.translate(px - size * 2, py - size * scale); + ctx.font = "100 " + 14 + "px Arial"; + ctx.textBaseline = 'top'; + + for (let i = 0; i < scores.length; i++) { + const score = scores[i].s1+"-"+scores[i].s2; + const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; + const text = ctx.measureText(score); + let dx = (size * 2 - text.width) / 2; + let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillStyle = '#ffffffdd'; + ctx.fillRect(dx, size * 2 * scale / div * (i) + dy, text.width, 14); + ctx.fillStyle = "#000000"; + ctx.fillText(score, dx, size * 2 * scale / div * (i) + dy, size * 2); + } + ctx.restore(); + } + + function drawNode(tree, px, py) { + ctx.beginPath() + ctx.moveTo(px, py) + ctx.lineTo(px - size, py) + ctx.stroke(); + + let death = tree.death() - 1 + let match = tree.data + + if (death === 0) { + ctx.beginPath() + ctx.moveTo(px - size, py + size) + ctx.lineTo(px - size, py - size) + + ctx.moveTo(px - size, py + size) + ctx.lineTo(px - size * 2, py + size) + ctx.moveTo(px - size, py - size) + ctx.lineTo(px - size * 2, py - size) + ctx.stroke(); + + printScores(match.scores, px, py, 1); + + ctx.fillStyle = "#FF0000"; + printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, false, true) + + ctx.fillStyle = "#0000FF"; + printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, false, true) + + if (max_y < py + size + ((size * 1.5 / 2) | 0)) max_y = py + size + (size * 1.5 / 2) | 0; + } else { + ctx.beginPath() + ctx.moveTo(px - size, py) + ctx.lineTo(px - size, py + size * 2 * death) + ctx.moveTo(px - size, py) + ctx.lineTo(px - size, py - size * 2 * death) + + ctx.moveTo(px - size, py + size * 2 * death) + ctx.lineTo(px - size * 2, py + size * 2 * death) + ctx.moveTo(px - size, py - size * 2 * death) + ctx.lineTo(px - size * 2, py - size * 2 * death) + ctx.stroke(); + + printScores(match.scores, px, py, 1.5); + + ctx.fillStyle = "#FF0000"; + printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, true, true) + + ctx.fillStyle = "#0000FF"; + printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, true, true) + + if (max_y < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + max_y = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + + ctx.stroke(); + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + function win(scores) { + let sum = 0; + for (const score of scores) { + if (score.s1 === -1000 || score.s2 === -1000) + continue; + + if (score.s1 > score.s2) + sum++; + else if (score.s1 < score.s2) + sum--; + } + return sum; + } + + let px = max_x; + let py; + let max_y + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + max_y = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + let win_name = ""; + if (node.data.end) { + if (win(node.data.scores) > 0) + win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName; + else + win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName; + } + + + ctx.fillStyle = "#18A918"; + printText(win_name, px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0), + size * 8, (size * 1.5) | 0, true, false); + + px = px - size * 2 - size * 8; + + drawNode(node, px, py); + + py = max_y + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = max_x; + } + } + + return canvas; +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index a844107..8454f11 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -11,8 +11,11 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import JSZip from "jszip"; import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx"; +import {faGlobe} from "@fortawesome/free-solid-svg-icons"; -export function CMAdmin() { +const vite_url = import.meta.env.VITE_URL; + +export function CMAdmin({compUuid}) { const [catId, setCatId] = useState(null); const [cat, setCat] = useState(null); const menuActions = useRef({}); @@ -46,7 +49,7 @@ export function CMAdmin() { - + } @@ -155,7 +158,7 @@ async function downloadResourcesAsZip(resourceList) { progressText.textContent = "Téléchargement terminé !"; } -function Menu({menuActions}) { +function Menu({menuActions, compUuid}) { const e = document.getElementById("actionMenu") const longPress = useRef({time: null, timer: null, button: null}); const obsModal = useRef(null); @@ -213,6 +216,16 @@ function Menu({menuActions}) { exportOBSConfiguration(adresse, password, assets_dir) } + const copyScriptToClipboard = () => { + navigator.clipboard.writeText(`
+ ` + ).then(() => { + toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress."); + }).catch(err => { + toast.error("Erreur lors de la copie dans le presse-papier : " + err); + }); + } + if (!e) return <>; return <> @@ -220,11 +233,16 @@ function Menu({menuActions}) { <>
longPressDown("obs")} onMouseUp={() => longPressUp("obs")} data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/> + copyScriptToClipboard()} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Copier le scripte d'intégration"/> , document.getElementById("actionMenu"))}