51 Commits

Author SHA1 Message Date
dcaa79245c Merge pull request 'dev' (#124) from dev into master
Reviewed-on: #124
2026-08-06 14:17:49 +00:00
2772e44456 feat: auto fill certificate fields on add licence
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 11m30s
2026-08-06 16:16:47 +02:00
8f0e3c0738 feat: show old member by default 2026-08-06 15:14:30 +02:00
81f6169d41 feat: add licence detail to add log 2026-08-06 15:03:03 +02:00
931c5076b7 feat: update libs 2026-08-06 15:02:44 +02:00
a328e71035 feat: add licence state to membre find result 2026-08-05 16:45:18 +02:00
72f8a27272 Merge pull request 'fix: cache refresh v2' (#123) from dev into master
Reviewed-on: #123
2026-03-23 21:12:00 +00:00
d3805ea0e7 fix: cache refresh v2
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m13s
2026-03-23 22:11:42 +01:00
a1e5006e95 Merge pull request 'fix: cache refresh' (#122) from dev into master
Reviewed-on: #122
2026-03-23 15:31:19 +00:00
2d883ca1da fix: cache refresh
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m10s
2026-03-23 16:29:11 +01:00
d357328e48 Merge pull request 'fix: add log in getAllClubArray' (#121) from dev into master
Reviewed-on: #121
2026-03-23 15:06:51 +00:00
2501b998fa fix: add log in getAllClubArray
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m8s
2026-03-23 16:05:09 +01:00
f9e4b83ad3 Merge pull request 'hot fix: remove save score button 2' (#120) from dev into master
Reviewed-on: #120
2026-03-21 11:09:17 +00:00
a7c89dbd73 hot fix: remove save score button 2
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m55s
2026-03-21 12:08:45 +01:00
ae087084e9 Merge pull request 'hot fix: remove save score button' (#119) from dev into master
Reviewed-on: #119
2026-03-21 11:00:12 +00:00
b2ee06d3b9 hot fix: remove save score button
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 44s
2026-03-21 11:59:37 +01:00
5afef2bcab Merge pull request 'fix: change obs lice name location' (#118) from dev into master
Reviewed-on: #118
2026-03-20 15:40:37 +00:00
3ed44bc044 fix: change obs lice name location
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m57s
2026-03-20 16:40:16 +01:00
55d39e02d2 Merge pull request 'fix: clear pub aff when no cat select' (#117) from dev into master
Reviewed-on: #117
2026-03-20 15:11:17 +00:00
d091503f19 fix: clear pub aff when no cat select
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m3s
2026-03-20 16:10:59 +01:00
d5c1273e18 Merge pull request 'fix: change methode for move upload file' (#116) from dev into master
Reviewed-on: #116
2026-03-18 12:07:58 +00:00
ea17989a82 fix: change methode for move upload file
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-03-18 13:07:39 +01:00
eda96d5ed2 Merge pull request 'fix: add more log for file copy' (#115) from dev into master
Reviewed-on: #115
2026-03-18 10:07:00 +00:00
e8555f7992 fix: add more log for file copy
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m0s
2026-03-18 11:06:40 +01:00
3a4e2f160f Merge pull request 'fix: add log for file copy' (#114) from dev into master
Reviewed-on: #114
2026-03-18 09:48:37 +00:00
fdd42c6a63 fix: add log for file copy
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m1s
2026-03-18 10:48:09 +01:00
2b84854ef2 Merge pull request 'fix: obs support wss' (#113) from dev into master
Reviewed-on: #113
2026-03-17 22:16:29 +00:00
ddf530ce4c fix: obs support wss
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m8s
2026-03-17 23:16:06 +01:00
fc3bea7f76 Merge pull request 'fix: language detection' (#112) from dev into master
Reviewed-on: #112
2026-03-16 11:19:50 +00:00
286911cf89 fix: language detection
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m2s
2026-03-16 12:19:30 +01:00
e67e01640a Merge pull request 'fix: club guest mixing in result' (#111) from dev into master
Reviewed-on: #111
2026-03-10 17:47:33 +00:00
4bfa6e845d fix: club guest mixing in result
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m3s
2026-03-10 13:59:35 +01:00
e870ffdda0 Merge pull request 'fix: eslint version' (#110) from dev into master
Reviewed-on: #110
2026-03-09 12:19:27 +00:00
806ead5b98 fix: eslint version
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m29s
2026-03-09 13:19:12 +01:00
378a17ba75 Merge pull request 'dev' (#109) from dev into master
Reviewed-on: #109
2026-03-09 12:10:46 +00:00
ee0a7d87e9 feat: cm Add audio encodeur generator
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 34s
2026-03-09 13:07:51 +01:00
812d873d5d fix: Match reorder refresh 2026-03-08 21:58:57 +01:00
d6d9f86254 feat: PDF for classement club 2026-02-27 11:10:31 +01:00
a22225ee7a feat: classement club 2026-02-23 12:01:36 +01:00
3cb12826d0 Merge pull request 'dev' (#108) from dev into master
Reviewed-on: #108
2026-02-20 16:59:40 +00:00
2f390b03e2 feat: add podium PDF
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m28s
2026-02-19 15:55:46 +01:00
ed5d73c25f feat: improve generation PDF for categories 2026-02-17 21:35:29 +01:00
0a368454c4 fix: duplicate id liceInput1 2026-02-16 22:49:49 +01:00
72b248d09d Merge pull request 'dev' (#107) from dev into master
Reviewed-on: #107
2026-02-06 16:18:39 +00:00
34a6911fa0 Merge pull request 'dev' (#106) from dev into master
Reviewed-on: #106
2026-01-30 14:07:08 +00:00
59ba31ae2d Merge pull request 'dev' (#105) from dev into master
Reviewed-on: #105
2026-01-23 15:49:24 +00:00
4b56aa0209 Merge pull request 'dev' (#104) from dev into master
Reviewed-on: #104
2026-01-18 15:19:12 +00:00
eefa77369a Merge pull request 'feat: update js npm lib' (#103) from dev into master
Reviewed-on: #103
2026-01-16 15:21:11 +00:00
a942798a6c Merge pull request 'feat: add match eq support' (#102) from dev into master
Reviewed-on: #102
2026-01-16 14:37:55 +00:00
876545630a Merge pull request 'fix: translate backend asset includes in native' (#101) from dev into master
Reviewed-on: #101
2026-01-16 13:22:09 +00:00
cad6d14ba8 Merge pull request 'fix: translate backend add log' (#100) from dev into master
Reviewed-on: #100
2026-01-16 12:59:33 +00:00
41 changed files with 2694 additions and 1737 deletions

View File

@@ -1,9 +1,11 @@
package fr.titionfire.ffsaf.data.model; package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.ResultPrivacy; import fr.titionfire.ffsaf.utils.ResultPrivacy;
public interface CombModel { public interface CombModel {
Long getCombId(); Long getCombId();
String getName(); String getName();
String getName(MembreModel model, ResultPrivacy privacy); String getName(MembreModel model, ResultPrivacy privacy);
Categorie getCategorie();
} }

View File

@@ -122,7 +122,9 @@ public class LicenceService {
.call(genLicenceNumberAndAccountIfNeed()) .call(genLicenceNumberAndAccountIfNeed())
: Uni.createFrom().nullItem() : Uni.createFrom().nullItem()
)) ))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(), .call(licenceModel -> ls.logA(LogModel.ActionType.ADD,
"%s (valid=%b, pay=%b, %s)" .formatted(membreModel.getObjectName(), model.isValidate(),
model.isPay(), model.getCertificate()),
licenceModel)); licenceModel));
}); });
} else { } else {

View File

@@ -184,11 +184,17 @@ public class MembreService {
String finalSearch = search; String finalSearch = search;
return getLicenceListe(licenceRequest, payState) return getLicenceListe(licenceRequest, payState)
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .chain(l -> {
.chain(ids -> {
PanacheQuery<MembreModel> query; PanacheQuery<MembreModel> query;
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit)); query = repository.find(queryStr, sort, finalSearch,
return getPageResult(query, limit, page); l.stream().map(l2 -> l2.getMembre().getId()).toList(), club).page(Page.ofSize(limit));
return getPageResult(query, limit, page)
.call(r -> (r.getResult().isEmpty() || !l.isEmpty() ?
Uni.createFrom().item(l) :
licenceRepository.list("saison = ?1 AND membre.id IN ?2", Utils.getSaison(),
r.getResult().stream().map(SimpleMembre::getId).toList()))
.invoke(l2 -> r.setAdditionalData(l2.stream().map(SimpleLicence::fromModel)))
);
}); });
} }
@@ -210,7 +216,7 @@ public class MembreService {
public Uni<List<SimpleMembreInOutData>> getAllExport(String subject) { public Uni<List<SimpleMembreInOutData>> getAllExport(String subject) {
return repository.find("userId = ?1", subject).firstResult() return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> repository.list("club = ?1", membreModel.getClub())) .chain(membreModel -> repository.list("club = ?1", membreModel.getClub()))
.chain(membres -> licenceRepository.list("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres) .chain(membres -> licenceRepository.list("membre IN ?1", membres)
.map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList())); .map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList()));
} }

View File

@@ -7,6 +7,8 @@ import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*; import fr.titionfire.ffsaf.utils.*;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Multi;
@@ -15,6 +17,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import lombok.Builder; import lombok.Builder;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*; import java.util.*;
@@ -52,6 +55,11 @@ public class ResultService {
@Inject @Inject
TradService trad; TradService trad;
@Inject
@CacheName("club-classement")
Cache cacheClubClassement;
private static final HashMap<String, Long> cacheClubClassementExp = new HashMap<>();
private static final HashMap<Long, String> combTempIds = new HashMap<>(); private static final HashMap<Long, String> combTempIds = new HashMap<>();
private static String getCombTempId(Long key) { private static String getCombTempId(Long key) {
@@ -188,6 +196,7 @@ public class ResultService {
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w, comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w,
stat.pointMake, stat.pointTake, stat.getPointRate()); stat.pointMake, stat.pointTake, stat.getPointRate());
}) })
.filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0)
.sorted(Comparator .sorted(Comparator
.comparing(ResultCategoryData.RankArray::getScore) .comparing(ResultCategoryData.RankArray::getScore)
.thenComparing(ResultCategoryData.RankArray::getWin) .thenComparing(ResultCategoryData.RankArray::getWin)
@@ -212,8 +221,8 @@ public class ResultService {
}); });
} }
private void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards, public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards,
ResultCategoryData out) { ResultCategoryData out) {
if ((categoryModel.getType() & 2) != 0) { if ((categoryModel.getType() & 2) != 0) {
AtomicInteger rank = new AtomicInteger(0); AtomicInteger rank = new AtomicInteger(0);
categoryModel.getTree().stream() categoryModel.getTree().stream()
@@ -230,7 +239,8 @@ public class ResultService {
} else { } else {
for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) { for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) {
for (ResultCategoryData.RankArray r : list) { for (ResultCategoryData.RankArray r : list) {
out.getClassement().add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName())); out.getClassement()
.add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName()));
} }
} }
} }
@@ -258,7 +268,7 @@ public class ResultService {
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(), .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
out.getClassement() out.getClassement()
.add(new ResultCategoryData.ClassementData(rank.getAndIncrement(), m.getC2(), .add(new ResultCategoryData.ClassementData(rank.get(), m.getC2(),
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))); m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
} }
} else { } else {
@@ -606,21 +616,32 @@ public class ResultService {
new ArrayList<>(), membreModel) new ArrayList<>(), membreModel)
))); )));
} else { } else {
return clubRepository.findById(id).chain(clubModel -> return cardRepository.list("competition.uuid = ?1", uuid)
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) .chain(cards -> clubRepository.findById(id).chain(clubModel ->
.chain(registers -> matchRepository.list( registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + .chain(registers -> matchRepository.list(
"WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)", "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
uuid, registers.stream().map(RegisterModel::getMembre).toList()) "WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid) uuid, registers.stream().map(RegisterModel::getMembre).toList())
.map(cards -> .map(matchModels -> matchModels.stream()
.map(m -> new MatchModelExtend(m, cards)))
.chain(s -> competitionGuestRepository.list(
"competition.uuid = ?1 AND club = ?2", uuid, clubModel.getName())
.chain(guests -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
uuid, guests)
.map(mm -> new Pair<>(guests, Stream.concat(s, mm.stream()
.map(m -> new MatchModelExtend(m, cards)))
.distinct().toList()))
))
.map(p ->
getClubArray2(clubModel.getName(), getClubArray2(clubModel.getName(),
registers.stream().map(o -> (CombModel) o.getMembre()) Stream.concat(
.toList(), registers.stream().map(RegisterModel::getMembre),
matchModels.stream() p.getKey().stream()
.map(m -> new MatchModelExtend(m, cards)).toList(), ).toList(),
registers, membreModel) p.getValue(), registers, membreModel)
)))); ))));
} }
} }
@@ -723,6 +744,112 @@ public class ResultService {
} }
} }
public Uni<List<ClubClassement>> getAllClubArray(String uuid) {
return getAllClubArray(uuid, true);
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> getAllClubArray(uuid, true));
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid, boolean cache) {
List<CardModel> cards = new java.util.ArrayList<>();
return (!cache || cacheClubClassementExp.getOrDefault(uuid, 0L) < System.currentTimeMillis() ?
cacheClubClassement.invalidate(uuid).invoke(() -> cacheClubClassementExp.remove(uuid)) :
Uni.createFrom().voidItem()
).chain(o -> cacheClubClassement.getAsync(uuid, k -> cardRepository.list("competition.uuid = ?1", uuid)
.invoke(__ -> System.out.println("Cache miss for club classement with uuid " + uuid))
.invoke(cards::addAll)
.chain(__ -> matchRepository.list("category.compet.uuid = ?1", uuid))
.chain(matchs -> {
HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
for (MatchModel match : matchs) {
if (!map.containsKey(match.getCategory()))
map.put(match.getCategory(), new java.util.ArrayList<>());
map.get(match.getCategory()).add(match);
}
return Multi.createFrom().iterable(map.entrySet())
.onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
.map(entry -> {
ResultCategoryData tmp = new ResultCategoryData();
getArray2(entry.getValue().stream().map(m -> new MatchModelExtend(m, cards))
.toList(),
null, tmp);
getClassementArray(entry.getKey(), null, cards, tmp);
return tmp;
})
.collect().asList();
})
.map(categoryData -> {
HashMap<String, ClubClassement> clubMap = new HashMap<>();
categoryData.forEach(
c -> c.getClassement().stream().map(ResultCategoryData.ClassementData::comb)
.filter(Objects::nonNull)
.distinct()
.map(comb -> {
if (comb instanceof MembreModel membreModel2) {
return (membreModel2.getClub() != null) ? membreModel2.getClub()
.getName() : "";
} else if (comb instanceof CompetitionGuestModel guestModel) {
return guestModel.getClub();
}
return "";
})
.filter(s -> s != null && !s.isBlank() && !s.equals("Team"))
.distinct()
.forEach(clubName -> clubMap.putIfAbsent(clubName,
new ClubClassement(clubName))));
categoryData.forEach(c -> c.getClassement().forEach(classementData -> {
if (classementData.rank() > 3)
return;
if (classementData.comb() != null) {
String clubName = "";
if (classementData.comb() instanceof MembreModel membreModel2) {
clubName = (membreModel2.getClub() != null) ? membreModel2.getClub()
.getName() : "";
} else if (classementData.comb() instanceof CompetitionGuestModel guestModel) {
clubName = guestModel.getClub();
}
if (clubName != null && !clubName.isBlank()
&& !clubName.equals("Team") && clubMap.containsKey(clubName)) {
ClubClassement entity = clubMap.get(clubName);
entity.score[classementData.rank() - 1]++;
entity.tt_score += 4 - classementData.rank();
}
}
}));
return clubMap.values().stream()
.sorted(Comparator.comparingInt((ClubClassement c) -> c.tt_score)
.thenComparingInt(c -> c.score[0])
.thenComparingInt(c -> c.score[1])
.thenComparingInt(c -> c.score[2]).reversed())
.toList();
})
.invoke(__ -> cacheClubClassementExp.put(uuid, System.currentTimeMillis() + 60 * 1000L))
));
}
@Data
@RegisterForReflection
public static class ClubClassement {
String name;
Integer[] score = new Integer[]{0, 0, 0};
int tt_score = 0;
public ClubClassement(String name) {
this.name = name;
}
}
@RegisterForReflection @RegisterForReflection
public static class CombStat { public static class CombStat {
public int w; public int w;

View File

@@ -8,6 +8,7 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
@Path("api/public/result/{id}") @Path("api/public/result/{id}")
public class ExternalResultEndpoints { public class ExternalResultEndpoints {
@@ -73,6 +74,13 @@ public class ExternalResultEndpoints {
return resultService.getClubList(id); return resultService.getClubList(id);
} }
@GET
@Path("/club/classement")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<ResultService.ClubClassement>> clubClassement() {
return resultService.getAllClubArray(id);
}
@GET @GET
@Path("/club/data") @Path("/club/data")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View File

@@ -47,6 +47,12 @@ public class ResultEndpoints {
return resultService.getClubList(uuid, securityCtx); return resultService.getClubList(uuid, securityCtx);
} }
@GET
@Path("{uuid}/club/classement")
public Uni<List<ResultService.ClubClassement>> getClubClassement(@PathParam("uuid") String uuid) {
return resultService.getAllClubArray(uuid, securityCtx);
}
@GET @GET
@Path("{uuid}/club/{id}") @Path("{uuid}/club/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) { public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {

View File

@@ -2,10 +2,14 @@ package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.LicenceModel; import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -24,7 +28,27 @@ public class SimpleMembreInOutData {
public static SimpleMembreInOutData fromModel(MembreModel membreModel, List<LicenceModel> lc) { public static SimpleMembreInOutData fromModel(MembreModel membreModel, List<LicenceModel> lc) {
LicenceModel currentLicence = lc.stream().filter(l -> l.getMembre().getId().equals(membreModel.getId())) LicenceModel currentLicence = lc.stream().filter(l -> l.getMembre().getId().equals(membreModel.getId()))
.findFirst().orElse(null); .max(Comparator.comparingInt(LicenceModel::getSaison)).orElse(null);
String certif = null;
if (currentLicence != null && currentLicence.getCertificate() != null) {
String[] strings = currentLicence.getCertificate().split("¤");
if (currentLicence.getSaison() == Utils.getSaison()) {
certif = currentLicence.getCertificate();
} else if (strings.length > 1) {
try {
Date date = new SimpleDateFormat("yyyy-MM-dd").parse(strings[1]);
Calendar max = Utils.getFirstDateOfSaison();
max.add(Calendar.YEAR, -2);
if (max.getTime().compareTo(date) < 0) {
certif = currentLicence.getCertificate();
}
} catch (Exception ignored) {
}
}
}
return new SimpleMembreInOutData( return new SimpleMembreInOutData(
membreModel.getLicence(), membreModel.getLicence(),
@@ -33,8 +57,8 @@ public class SimpleMembreInOutData {
membreModel.getEmail(), membreModel.getEmail(),
membreModel.getGenre().str, membreModel.getGenre().str,
membreModel.getBirth_date(), membreModel.getBirth_date(),
currentLicence != null, currentLicence != null && currentLicence.getSaison() == Utils.getSaison(),
currentLicence == null ? null : currentLicence.getCertificate() certif
); );
} }
} }

View File

@@ -10,13 +10,14 @@ import java.util.List;
@Data @Data
@RegisterForReflection @RegisterForReflection
public class PageResult<T> { public class PageResult<T> {
@Schema(description = "Le numéro de la page courante.", example = "1") @Schema(description = "Le numéro de la page courante.", examples = "1")
private int page; private int page;
@Schema(description = "Le nombre d'éléments par page.", example = "10") @Schema(description = "Le nombre d'éléments par page.", examples = "10")
private int page_size; private int page_size;
@Schema(description = "Le nombre total de pages.", example = "5") @Schema(description = "Le nombre total de pages.", examples = "5")
private int page_count; private int page_count;
@Schema(description = "Le nombre total d'éléments.", example = "47") @Schema(description = "Le nombre total d'éléments.", examples = "47")
private long result_count; private long result_count;
private List<T> result = new ArrayList<>(); private List<T> result = new ArrayList<>();
private Object additionalData;
} }

View File

@@ -20,6 +20,8 @@ import java.io.*;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -117,6 +119,15 @@ public class Utils {
} }
} }
public static Calendar getFirstDateOfSaison() {
Calendar calendar = Calendar.getInstance();
calendar.set(getSaison(), Calendar.SEPTEMBER, 1, 0, 0, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
public static Calendar toCalendar(Date date) { public static Calendar toCalendar(Date date) {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(date); cal.setTime(date);
@@ -244,15 +255,16 @@ public class Utils {
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + "."); FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter); File[] files = dirFile.listFiles(filter);
if (files != null) { if (files != null) {
for (File f : files) { for (File f2 : files) {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
f.delete(); f2.delete();
} }
} }
File f = file.filePath().toFile(); Path f = file.filePath();
//noinspection ResultOfMethodCallIgnored System.out.println(f + " -> " + new File(dirFile, id + "." + detectedExtensions[0]));
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0])); Files.copy(f, new File(dirFile, id + "." + detectedExtensions[0]).toPath(),
StandardCopyOption.REPLACE_EXISTING);
return "ok"; return "ok";
}))); })));
} }

View File

@@ -54,6 +54,9 @@ public class CompetitionWS {
@Inject @Inject
RState rState; RState rState;
@Inject
RPDF rpdf;
@Inject @Inject
SecurityCtx securityCtx; SecurityCtx securityCtx;
@@ -99,6 +102,7 @@ public class CompetitionWS {
getWSReceiverMethods(RCard.class, rCard); getWSReceiverMethods(RCard.class, rCard);
getWSReceiverMethods(RTeam.class, rTeam); getWSReceiverMethods(RTeam.class, rTeam);
getWSReceiverMethods(RState.class, rState); getWSReceiverMethods(RState.class, rState);
getWSReceiverMethods(RPDF.class, rpdf);
executor = notifyExecutor; executor = notifyExecutor;
} }

View File

@@ -0,0 +1,112 @@
package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CardModel;
import fr.titionfire.ffsaf.data.model.CategoryModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.CardRepository;
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.domain.entity.MatchModelExtend;
import fr.titionfire.ffsaf.domain.service.ResultService;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.ws.PermLevel;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
@RegisterForReflection
public class RPDF {
@Inject
CompetitionRepository competitionRepository;
@Inject
MatchRepository matchRepository;
@Inject
CardRepository cardRepository;
@Inject
CategoryRepository categoryRepository;
@Inject
ResultService resultService;
@Inject
TradService trad;
@Transactional
@WSReceiver(code = "getPodium", permission = PermLevel.VIEW)
public Uni<List<PodiumEntity>> getPodium(WebSocketConnection connection, Object o) {
List<CardModel> cards = new java.util.ArrayList<>();
return cardRepository.list("competition.uuid = ?1", connection.pathParam("uuid"))
.invoke(cards::addAll)
.chain(__ -> matchRepository.list("category.compet.uuid = ?1", connection.pathParam("uuid")))
.chain(matchs -> {
HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
for (MatchModel match : matchs) {
if (!map.containsKey(match.getCategory()))
map.put(match.getCategory(), new java.util.ArrayList<>());
map.get(match.getCategory()).add(match);
}
return Multi.createFrom().iterable(map.entrySet())
.onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
.map(entry -> {
ResultCategoryData tmp = new ResultCategoryData();
double cmoy = entry.getValue().stream().flatMap(m -> Stream.of(m.getC1(), m.getC2()))
.filter(c -> c != null && c.getCategorie() != null)
.mapToInt(c -> c.getCategorie().ordinal())
.average().orElse(0);
Categorie categorie_moy = Categorie.values()[(int) Math.ceil(cmoy)];
resultService.getArray2(
entry.getValue().stream().map(m -> new MatchModelExtend(m, cards)).toList(),
null, tmp);
resultService.getClassementArray(entry.getKey(), null, cards, tmp);
String source = "";
if ((entry.getKey().getType() & 2) != 0) {
if (entry.getKey().isTreeAreClassement())
source = trad.t("podium.source.classement", connection);
else
source = trad.t("podium.source.tree", connection);
} else if ((entry.getKey().getType() & 1) != 0)
source = trad.t("podium.source.poule", connection);
return new PodiumEntity(entry.getKey().getName(), source, categorie_moy,
tmp.getClassement());
})
.collect().asList();
});
}
@WSReceiver(code = "getPodiumClub", permission = PermLevel.VIEW)
public Uni<List<ResultService.ClubClassement>> getPodiumClub(WebSocketConnection connection, Object o) {
return resultService.getAllClubArray(connection.pathParam("uuid"), false);
}
@RegisterForReflection
public static record PodiumEntity(String poule_name, String source, Categorie categorie,
List<ResultCategoryData.ClassementData> podium) {
}
}

View File

@@ -89,3 +89,6 @@ carton.non.trouver=Card not found
card.cannot.be.added=Unable to add the card card.cannot.be.added=Unable to add the card
configuration.non.supportee=Unsupported configuration configuration.non.supportee=Unsupported configuration
err.match.termine=Error, a placement match has already been played err.match.termine=Error, a placement match has already been played
podium.source.classement=Ranking
podium.source.tree=Tournaments
podium.source.poule=Pool

View File

@@ -85,3 +85,6 @@ carton.non.trouver=Carton introuvable
card.cannot.be.added=Impossible d'ajouter le carton card.cannot.be.added=Impossible d'ajouter le carton
configuration.non.supportee=Configuration non supportée configuration.non.supportee=Configuration non supportée
err.match.termine=Erreur, un match de classement a déjà été joué err.match.termine=Erreur, un match de classement a déjà été joué
podium.source.classement=Classement
podium.source.tree=Tournois
podium.source.poule=Poule

File diff suppressed because it is too large Load Diff

View File

@@ -13,41 +13,42 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.3.1",
"@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.3.1",
"@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.3.1",
"@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.3.1",
"@fortawesome/react-fontawesome": "^3.1.1", "@fortawesome/react-fontawesome": "^3.5.0",
"axios": "^1.13.2", "axios": "1.18.0",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"i18next": "^25.8.0", "i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^4.0.1",
"jspdf": "^4.1.0", "jspdf": "4.2.1",
"jspdf-autotable": "^5.0.8",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7", "obs-websocket-js": "^5.0.8",
"proj4": "^2.20.2", "proj4": "^2.21.0",
"react": "^19.2.3", "react": "^19.2.8",
"react-dom": "^19.2.3", "react-dom": "^19.2.8",
"react-i18next": "^16.5.3", "react-i18next": "^17.0.11",
"react-is": "^19.2.3", "react-is": "^19.2.8",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-loader-spinner": "^8.0.2", "react-loader-spinner": "^8.0.2",
"react-router-dom": "^7.12.0", "react-router-dom": "^7.18.2",
"react-toastify": "^11.0.5", "react-toastify": "^11.1.0",
"recharts": "^3.7.0", "recharts": "^3.10.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"xlsx-js-style": "^1.2.0" "xlsx-js-style": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.9", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^6.0.5",
"eslint": "^9.39.2", "eslint": "^9.39.5",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.5.3",
"vite": "^7.3.1" "vite": "8.0.16"
} }
} }

View File

@@ -39,6 +39,9 @@ function setSubPage(name) {
case 'club': case 'club':
clubPage(location); clubPage(location);
break; break;
case 'clubRank':
clubRankPage();
break;
case 'all': case 'all':
combsPage(); combsPage();
break; break;
@@ -54,6 +57,7 @@ function homePage() {
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li> <li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li>
<li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li> <li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li>
<li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li> <li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li>
<li><a id="clubClassement" href="javascript:void(0);">${i18next.t('classementClub')}</a></li>
<li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li> <li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
</ul> </ul>
` `
@@ -62,6 +66,7 @@ function homePage() {
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule')); document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb')); document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club')); document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
document.getElementById('clubClassement').addEventListener('click', () => setSubPage('clubRank'));
document.getElementById('allLink').addEventListener('click', () => setSubPage('all')); document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
} }
@@ -640,6 +645,60 @@ function clubPage(location) {
rootDiv.append(content) rootDiv.append(content)
} }
function buildClubsView(clubs) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>${i18next.t('classementDesClub')} :</h3>
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
<table style="width: 1200px;overflow: auto"><thead>
<tr>
<th class="has-text-align-center" data-align="center">${i18next.t('club')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('1er')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('2eme')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('3eme')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
</tr>
</thead><tbody>`
for (const club of clubs) {
arrayContent += `
<tr>
<td class="has-text-align-center" data-align="center">${club.name}</td>
<td class="has-text-align-center" data-align="center">${club.score[0]}</td>
<td class="has-text-align-center" data-align="center">${club.score[1]}</td>
<td class="has-text-align-center" data-align="center">${club.score[2]}</td>
<td class="has-text-align-center" data-align="center">${club.tt_score}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
pouleDiv.innerHTML = arrayContent;
return pouleDiv;
}
function clubRankPage() {
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
const content = document.createElement('div');
content.style.marginTop = '1em';
const dataContainer = document.createElement('div');
dataContainer.id = 'data-container';
const loading = startLoading(content);
fetch(`${apiUrlRoot}/club/classement`)
.then(response => response.json())
.then(clubs => {
console.log(clubs);
dataContainer.replaceChildren(buildClubsView(clubs));
})
.catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaListe'))))
.finally(() => stopLoading(loading));
content.append(dataContainer);
rootDiv.append(content)
}
function buildCombsView(combs) { function buildCombsView(combs) {
const pouleDiv = document.createElement('div'); const pouleDiv = document.createElement('div');
let arrayContent = ` let arrayContent = `

View File

@@ -25,6 +25,7 @@
"cartonRouge": "Red card", "cartonRouge": "Red card",
"catégorie": "Category", "catégorie": "Category",
"catégorieDâgeMoyenne": "Middle-aged category", "catégorieDâgeMoyenne": "Middle-aged category",
"catégorieSélectionnée": "Selected category",
"catégoriesVontêtreCréées": "weight categories will be created", "catégoriesVontêtreCréées": "weight categories will be created",
"ceCartonEstIssuDunCartonDéquipe": "This card comes from a team card, do you really want to delete it?", "ceCartonEstIssuDunCartonDéquipe": "This card comes from a team card, do you really want to delete it?",
"certainsCombattantsNontPasDePoidsRenseigné": "Some fighters do not have a weight listed; they will NOT be included in the categories.", "certainsCombattantsNontPasDePoidsRenseigné": "Some fighters do not have a weight listed; they will NOT be included in the categories.",
@@ -86,6 +87,7 @@
"etatDesTablesDeMarque": "State of marque tables", "etatDesTablesDeMarque": "State of marque tables",
"exporter": "Export", "exporter": "Export",
"fermer": "Close", "fermer": "Close",
"feuilleVierge": "Blank sheet",
"finalesUniquement": "Finals only", "finalesUniquement": "Finals only",
"genre": "Gender", "genre": "Gender",
"genre.f": "F", "genre.f": "F",
@@ -95,6 +97,7 @@
"individuelle": "Individual", "individuelle": "Individual",
"informationCatégorie": "Category information", "informationCatégorie": "Category information",
"inscrit": "Registered", "inscrit": "Registered",
"jusquauRang": "Up to the rank",
"leTournoiServiraDePhaseFinaleAuxPoules": "The tournament will serve as the final phase for the group stage.", "leTournoiServiraDePhaseFinaleAuxPoules": "The tournament will serve as the final phase for the group stage.",
"lesCombattantsEnDehors": "Fighters not participating in the tournament will have a ranking match.", "lesCombattantsEnDehors": "Fighters not participating in the tournament will have a ranking match.",
"lesCombattantsEnDehors2": "Fighters outside the ranking tournament will have a ranking match", "lesCombattantsEnDehors2": "Fighters outside the ranking tournament will have a ranking match",
@@ -117,10 +120,13 @@
"obs.préfixDesSources": "Source prefix", "obs.préfixDesSources": "Source prefix",
"pays": "Country", "pays": "Country",
"personnaliser": "Personalize", "personnaliser": "Personalize",
"podium": "Podium",
"podiumDesClubs": "Club podium",
"poids": "Weight", "poids": "Weight",
"poule": "Pool", "poule": "Pool",
"poulePour": "Pool for: ", "poulePour": "Pool for: ",
"préparation...": "Preparing...", "préparation...": "Preparing...",
"quoiImprimer?": "What print?",
"remplacer": "Replace", "remplacer": "Replace",
"rouge": "Red", "rouge": "Red",
"réinitialiser": "Reset", "réinitialiser": "Reset",
@@ -140,6 +146,7 @@
"select.sélectionnerDesCombatants": "Select fighters", "select.sélectionnerDesCombatants": "Select fighters",
"select.à": "to", "select.à": "to",
"serveur": "Server", "serveur": "Server",
"source": "Source",
"suivant": "Next", "suivant": "Next",
"supprimer": "Delete", "supprimer": "Delete",
"supprimerUn": "Delete one", "supprimerUn": "Delete one",
@@ -164,6 +171,9 @@
"toast.matchs.create.error": "Error while creating matches.", "toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...", "toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.", "toast.matchs.create.success": "Matches created successfully.",
"toast.print.error": "Error while preparing print",
"toast.print.pending": "Preparing print...",
"toast.print.success": "Print ready!",
"toast.team.update.error": "Error while updating team", "toast.team.update.error": "Error while updating team",
"toast.team.update.pending": "Updating team...", "toast.team.update.pending": "Updating team...",
"toast.team.update.success": "Team updated!", "toast.team.update.success": "Team updated!",
@@ -183,6 +193,8 @@
"tournois": "Tournaments", "tournois": "Tournaments",
"tousLesMatchs": "All matches", "tousLesMatchs": "All matches",
"toutConserver": "Keep all", "toutConserver": "Keep all",
"touteLaCatégorie": "The entire category",
"toutesLesCatégories": "All categories",
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
"ttm.admin.scripte": "Copy integration script", "ttm.admin.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen", "ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",

View File

@@ -2,6 +2,9 @@
"--sélectionnerUnClub--": "--Select a club--", "--sélectionnerUnClub--": "--Select a club--",
"--sélectionnerUnCombattant--": "--Select a fighter--", "--sélectionnerUnCombattant--": "--Select a fighter--",
"--sélectionnerUneCatégorie--": "--Select a category--", "--sélectionnerUneCatégorie--": "--Select a category--",
"1er": "Gold medal",
"2eme": "Silver medal",
"3eme": "Bronze medal",
"abs.": "abs.", "abs.": "abs.",
"adversaire": "Opponent", "adversaire": "Opponent",
"aujourdhuià": "Today at {{time}}", "aujourdhuià": "Today at {{time}}",
@@ -11,7 +14,9 @@
"catégorie": "Category", "catégorie": "Category",
"chargement": "Loading", "chargement": "Loading",
"classement": "Ranking", "classement": "Ranking",
"classementFinal": "Final standings", "classementClub": "Club ranking",
"classementDesClub": "Clubs ranking",
"classementFinal": "Final ranking",
"club": "Club", "club": "Club",
"combattant": "Fighter", "combattant": "Fighter",
"combattants": "Fighters", "combattants": "Fighters",

View File

@@ -25,6 +25,7 @@
"cartonRouge": "Carton rouge", "cartonRouge": "Carton rouge",
"catégorie": "Catégorie", "catégorie": "Catégorie",
"catégorieDâgeMoyenne": "Catégorie d'âge moyenne", "catégorieDâgeMoyenne": "Catégorie d'âge moyenne",
"catégorieSélectionnée": "Catégorie sélectionnée",
"catégoriesVontêtreCréées": "catégories de poids vont être créées", "catégoriesVontêtreCréées": "catégories de poids vont être créées",
"ceCartonEstIssuDunCartonDéquipe": "Ce carton est issu d'un carton d'équipe, voulez-vous vraiment le supprimer ?", "ceCartonEstIssuDunCartonDéquipe": "Ce carton est issu d'un carton d'équipe, voulez-vous vraiment le supprimer ?",
"certainsCombattantsNontPasDePoidsRenseigné": "Certains combattants n'ont pas de poids renseigné, ils ne seront PAS insert dans les catégories", "certainsCombattantsNontPasDePoidsRenseigné": "Certains combattants n'ont pas de poids renseigné, ils ne seront PAS insert dans les catégories",
@@ -86,6 +87,7 @@
"etatDesTablesDeMarque": "Etat des tables de marque", "etatDesTablesDeMarque": "Etat des tables de marque",
"exporter": "Exporter", "exporter": "Exporter",
"fermer": "Fermer", "fermer": "Fermer",
"feuilleVierge": "Feuille vierge",
"finalesUniquement": "Finales uniquement", "finalesUniquement": "Finales uniquement",
"genre": "Genre", "genre": "Genre",
"genre.f": "F", "genre.f": "F",
@@ -95,6 +97,7 @@
"individuelle": "Individuelle", "individuelle": "Individuelle",
"informationCatégorie": "Information catégorie", "informationCatégorie": "Information catégorie",
"inscrit": "Inscrit", "inscrit": "Inscrit",
"jusquauRang": "Jusqu'au rang",
"leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules", "leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules",
"lesCombattantsEnDehors": "Les combattants en dehors du tournoi auront un match de classement", "lesCombattantsEnDehors": "Les combattants en dehors du tournoi auront un match de classement",
"lesCombattantsEnDehors2": "Les combattants en dehors du tournoi de classement auront un match de classement", "lesCombattantsEnDehors2": "Les combattants en dehors du tournoi de classement auront un match de classement",
@@ -117,10 +120,13 @@
"obs.préfixDesSources": "Préfix des sources", "obs.préfixDesSources": "Préfix des sources",
"pays": "Pays", "pays": "Pays",
"personnaliser": "Personnaliser", "personnaliser": "Personnaliser",
"podium": "Podium",
"podiumDesClubs": "Podium des clubs",
"poids": "Poids", "poids": "Poids",
"poule": "Poule", "poule": "Poule",
"poulePour": "Poule pour: ", "poulePour": "Poule pour: ",
"préparation...": "Préparation...", "préparation...": "Préparation...",
"quoiImprimer?": "Quoi imprimer ?",
"remplacer": "Remplacer", "remplacer": "Remplacer",
"rouge": "Rouge", "rouge": "Rouge",
"réinitialiser": "Réinitialiser", "réinitialiser": "Réinitialiser",
@@ -140,6 +146,7 @@
"select.sélectionnerDesCombatants": "Sélectionner des combatants", "select.sélectionnerDesCombatants": "Sélectionner des combatants",
"select.à": "à", "select.à": "à",
"serveur": "Serveur", "serveur": "Serveur",
"source": "Source",
"suivant": "Suivant", "suivant": "Suivant",
"supprimer": "Supprimer", "supprimer": "Supprimer",
"supprimerUn": "Supprimer un", "supprimerUn": "Supprimer un",
@@ -164,6 +171,9 @@
"toast.matchs.create.error": "Erreur lors de la création des matchs.", "toast.matchs.create.error": "Erreur lors de la création des matchs.",
"toast.matchs.create.pending": "Création des matchs en cours...", "toast.matchs.create.pending": "Création des matchs en cours...",
"toast.matchs.create.success": "Matchs créés avec succès.", "toast.matchs.create.success": "Matchs créés avec succès.",
"toast.print.error": "Erreur lors de la génération du PDF",
"toast.print.pending": "Génération du PDF en cours...",
"toast.print.success": "PDF généré !",
"toast.team.update.error": "Erreur lors de la mise à jour de l'équipe", "toast.team.update.error": "Erreur lors de la mise à jour de l'équipe",
"toast.team.update.pending": "Mise à jour de l'équipe...", "toast.team.update.pending": "Mise à jour de l'équipe...",
"toast.team.update.success": "Équipe mise à jour !", "toast.team.update.success": "Équipe mise à jour !",
@@ -183,6 +193,8 @@
"tournois": "Tournois", "tournois": "Tournois",
"tousLesMatchs": "Tous les matchs", "tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver", "toutConserver": "Tout conserver",
"touteLaCatégorie": "Toute la catégorie",
"toutesLesCatégories": "Toutes les catégories",
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
"ttm.admin.scripte": "Copier le scripte d'intégration", "ttm.admin.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran", "ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",

View File

@@ -2,6 +2,9 @@
"--sélectionnerUnClub--": "--Sélectionner un club--", "--sélectionnerUnClub--": "--Sélectionner un club--",
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--", "--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
"--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--", "--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--",
"1er": "Médaille d'or",
"2eme": "Médaille d'argent",
"3eme": "Médaille de bronze",
"abs.": "abs.", "abs.": "abs.",
"adversaire": "Adversaire", "adversaire": "Adversaire",
"aujourdhuià": "Aujourd'hui à {{time}}", "aujourdhuià": "Aujourd'hui à {{time}}",
@@ -11,6 +14,8 @@
"catégorie": "Catégorie", "catégorie": "Catégorie",
"chargement": "Chargement", "chargement": "Chargement",
"classement": "Classement", "classement": "Classement",
"classementClub": "Classement club",
"classementDesClub": "Classement des clubs",
"classementFinal": "Classement final", "classementFinal": "Classement final",
"club": "Club", "club": "Club",
"combattant": "Combattant", "combattant": "Combattant",

View File

@@ -0,0 +1,84 @@
class ProcessorDTMF extends AudioWorkletProcessor {
constructor() {
super();
this.sampleRate = sampleRate;
this.symbolDuration = 0.03; // 50 ms par symbole
this.samplesPerSymbol = Math.floor(this.sampleRate * this.symbolDuration);
this.encodeLowPrio = [];
this.symbolSamples = [];
this.lastBlackStep = 0;
this.port.onmessage = (e) => {
if (e.data.type === 'encode') {
this.symbolSamples.push(...this.encodeSymbols(e.data.symbols));
this.symbolSamples.push(...this.encodeBlack(this.sampleRate * 0.02));
}
if (e.data.type === 'encodeLowPrio') {
this.encodeLowPrio.push(e.data.symbols);
}
};
}
dtmfFrequencies = [
[697, 770, 852, 941], // Fréquences basses
[1209, 1336, 1477, 1633] // Fréquences hautes
];
encodeSymbols(symbols) {
const samples = [];
for (const symbol of symbols) {
const lf = this.dtmfFrequencies[0][symbol % 4]; // Fréquence basse
const hf = this.dtmfFrequencies[1][Math.floor(symbol / 4)]; // Fréquence haute
// console.log(`Symbol: ${symbol}, LF: ${lf} Hz, HF: ${hf} Hz`);
for (let i = 0; i < this.samplesPerSymbol; i++) {
const t = i / this.sampleRate;
const t2 = (this.lastBlackStep + i) / this.sampleRate;
samples.push(0.5 * Math.sin(2 * Math.PI * lf * t) + 0.5 * Math.sin(2 * Math.PI * hf * t) // Signal DTMF
+ Math.sin(2 * Math.PI * 150 * t2) * (0.0625 * (Math.sin(2 * Math.PI * 0.5 * t2) + 1))); // Ajouter un signal à 150 Hz pour le "black"
}
this.lastBlackStep += this.samplesPerSymbol;
// ajouter un silence de 10 ms entre les symboles
samples.push(...this.encodeBlack(this.sampleRate * 0.01)); // Silence
}
return samples;
}
encodeBlack(size) {
const samples = [];
for (let i = 0; i < size; i++) {
const t = (this.lastBlackStep + i) / this.sampleRate;
samples.push(Math.sin(2 * Math.PI * 150 * t) * (0.0625 * (Math.sin(2 * Math.PI * 0.5 * t) + 1))); // Signal à 350 Hz pour le "black"
}
this.lastBlackStep += size;
this.lastBlackStep %= this.sampleRate * 2; // Réinitialiser tous les 2 secondes pour éviter les débordements
return samples;
}
process(inputs, outputs, parameters) {
const output = outputs[0]; // output est un tableau de canaux (ex: [Float32Array, ...])
const channelData = output[0]; // Accéder au premier canal (mono)
if (this.symbolSamples.length === 0 && this.encodeLowPrio.length > 0) {
this.symbolSamples.push(...this.encodeSymbols(this.encodeLowPrio.shift()));
this.symbolSamples.push(...this.encodeBlack(this.sampleRate * 0.02));
}
if (this.symbolSamples.length === 0) {
const samples = this.encodeBlack(channelData.length)
for (let i = 0; i < channelData.length; i++) {
channelData[i] = samples[i] || 0;
}
return true;
}
for (let i = 0; i < channelData.length; i++) {
channelData[i] = this.symbolSamples.shift() || 0; // Prendre le prochain échantillon ou 0 si vide
}
return true;
}
}
registerProcessor('dtmf-processor', ProcessorDTMF);

View File

@@ -0,0 +1,207 @@
import React, {useEffect, useRef, useState} from 'react';
import {useTablesState} from "../../pages/competition/editor/StateWindow.jsx";
import {timePrint} from "../../utils/Tools.js";
let initialized = false;
const AudioEncoder = () => {
const audioContextRef = useRef(null);
const qpskProcessorRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const [table, setTable] = useState('1');
const lastSend = useRef({id: 0});
const {state} = useTablesState();
// Initialisation de l'AudioContext et du AudioWorklet
useEffect(() => {
const initAudio = async () => {
if (initialized)
return;
console.log("Initialisation de l'audio après interaction utilisateur");
initialized = true;
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log("AudioContext state:", audioContext.state);
if (audioContext.state === 'suspended') {
await audioContext.resume(); // Nécessaire pour démarrer le contexte audio
console.log("AudioContext resumed");
}
await audioContext.audioWorklet.addModule('/processor-dtmf.js');
await new Promise(r => setTimeout(r, 100));
const processor = new AudioWorkletNode(audioContext, 'dtmf-processor');
processor.connect(audioContext.destination);
qpskProcessorRef.current = processor;
audioContextRef.current = audioContext;
await new Promise(r => setTimeout(r, 100));
setIsReady(true);
} catch (err) {
initialized = false;
console.error("Erreur d'initialisation AudioWorklet:", err);
}
};
// Initialiser après un clic utilisateur (pour contourner les restrictions des navigateurs)
const handleUserInteraction = () => {
document.removeEventListener('click', handleUserInteraction);
initAudio();
};
document.addEventListener('click', handleUserInteraction);
return () => {
if (audioContextRef.current?.state !== 'closed') {
audioContextRef.current?.close();
}
};
}, []);
// Fonction pour encoder et envoyer un message
const encodeAndSend = (data, lowPrio = false) => {
if (!isReady) return;
const symbols = Array.from(data).flatMap(byte => [byte >> 4, byte & 0x0F]);
console.log("Bits :", symbols);
// 5. Envoyer les symboles au processeur audio
if (lowPrio) {
qpskProcessorRef.current.port.postMessage({type: 'encodeLowPrio', symbols});
} else {
qpskProcessorRef.current.port.postMessage({type: 'encode', symbols});
}
}
useEffect(() => {
const t = state.find(o => o.liceName === table)
if (!t)
return
// console.log("Data for table 1:", t, t.selectedMatch)
const last = lastSend.current;
if (t.selectedMatch !== last.id) {
clearTimeout(last.time_id)
last.time_id = setTimeout(() => {
last.time_id = null
if (t.selectedMatch === null) {
encodeAndSend(new Uint8Array([0, 0, 0, 0, 0, 0, 0]));
} else {
const data = [];
for (let i = 0; i < 7; i++) { // MaxSafeInteger est sur 7 bytes (53 bits de précision)
data.unshift(Number((BigInt(t.selectedMatch) >> BigInt(i * 8)) & 0xFFn))
}
data[0] = data[0] & 0x1F // 3 premiers bits à 0 pour différencier des autres types de messages (ex: score, chrono, etc.)
// console.log("Data to send (selectedMatch):", data)
encodeAndSend(new Uint8Array(data), false);
}
}, 250)
last.id = t.selectedMatch
}
const isRunning = (c) => c.startTime !== 0
const getTime = (c) => {
if (c.startTime === 0)
return c.time
return c.time + Date.now() - c.startTime
}
const timeStr = last.chronoState ? timePrint((last.chronoState.state === 2) ? last.chronoState.configPause : last.chronoState.configTime - getTime(last.chronoState)) : "-"
const timeStr2 = timePrint((t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState))
if (timeStr !== timeStr2) {
clearInterval(lastSend.current.time_chronoInter)
clearTimeout(lastSend.current.time_chronoText)
lastSend.current.time_chronoText = setTimeout(() => {
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
const ms = time % 1000
time = (time - ms) / 1000
const data = [((time >> 8) & 0x1F) + 0x20, time & 0xFF];
// console.log("Data to send (time):", data)
encodeAndSend(new Uint8Array(data));
lastSend.current.time_chronoInter = setInterval(() => {
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
const ms = time % 1000
time = (time - ms) / 1000
const data = [((time >> 8) & 0x1F) + 0x20, time & 0xFF];
// console.log("Data to send (time-auto):", data)
encodeAndSend(new Uint8Array(data), true);
}, 10000);
}, 150)
}
if (!last.chronoState || last.chronoState.state !== t.chronoState.state || isRunning(last.chronoState) !== isRunning(t.chronoState)) {
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
const ms = Math.round((time % 1000) / 250)
const data = [0x40 + (t.chronoState.state << 3) + (isRunning(t.chronoState) << 2) + (ms & 0x03)];
// console.log("Data to send (chrono state):", data)
encodeAndSend(new Uint8Array(data));
}
last.chronoState = {...t.chronoState}
// console.log(timeStr, timeStr2)
// console.log(last.chronoState, t.chronoState)
if (last.scoreRouge !== t.scoreState.scoreRouge) {
clearTimeout(last.time_sr)
last.time_sr = setTimeout(() => {
if (last.scoreRouge !== t.scoreState.scoreRouge) {
const b = t.scoreState.scoreRouge < 0
const s = b ? -t.scoreState.scoreRouge : t.scoreState.scoreRouge
const data = [0x60 + (b << 3) + ((s >> 8) & 0x07), (s & 0xFF)];
console.log("Data to send (score r):", data)
encodeAndSend(new Uint8Array(data), true);
last.scoreRouge = t.scoreState.scoreRouge
}
}, 250)
}
if (last.scoreBleu !== t.scoreState.scoreBleu) {
clearTimeout(last.time_sb)
last.time_sb = setTimeout(() => {
if (last.scoreBleu !== t.scoreState.scoreBleu) {
const b = t.scoreState.scoreBleu < 0
const s = b ? -t.scoreState.scoreBleu : t.scoreState.scoreBleu
const data = [0x60 + 0x10 + (b << 3) + ((s >> 8) & 0x07), (s & 0xFF)];
console.log("Data to send (score b):", data)
encodeAndSend(new Uint8Array(data), true);
last.scoreBleu = t.scoreState.scoreBleu
}
}, 250)
}
}, [state])
useEffect(() => {
const last = lastSend.current;
clearTimeout(last.scoreBleu)
clearTimeout(last.scoreRouge)
clearTimeout(last.time_id)
clearTimeout(last.time_chronoText)
clearInterval(last.time_chronoInter)
last.id = 0
last.scoreBleu = 0
last.scoreRouge = 0
last.chronoState = null
}, [table])
return (
<div>
<input
type="text"
value={table}
onChange={(e) => setTable(e.target.value)}
placeholder="Nom de la zone"
/>
<span>{isReady ? 'Actif' : "Zone non configurée"}</span>
</div>
);
};
export default AudioEncoder;

View File

@@ -663,8 +663,8 @@ export function AutoNewCatModalContent() {
</>} </>}
<div className="mb-3"> <div className="mb-3">
<label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label> <label htmlFor="liceInput2" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice} <input type="text" className="form-control" id="liceInput2" placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/> onChange={e => setLice(e.target.value)}/>
</div> </div>
@@ -774,8 +774,8 @@ export function AutoNewCatSModalContent() {
</div> </div>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label> <label htmlFor="liceInput3" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice} <input type="text" className="form-control" id="liceInput3" placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/> onChange={e => setLice(e.target.value)}/>
</div> </div>

View File

@@ -15,7 +15,7 @@ export function ListPresetSelect({disabled, value, onChange, returnId = true}) {
value={returnId ? value : (value ? value.id : -1)} value={returnId ? value : (value ? value.id : -1)}
onChange={e => { onChange={e => {
if (returnId) { if (returnId) {
onChange(e.target.value) onChange(Number(e.target.value))
} else { } else {
onChange(data.find(c => c.id === Number(e.target.value))) onChange(data.find(c => c.id === Number(e.target.value)))
} }

View File

@@ -6,8 +6,18 @@ import {initReactI18next} from 'react-i18next';
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
const options = { const options = {
order: [ 'querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'], order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
caches: [], caches: [],
convertDetectedLanguage: (lng) => {
const supportedLngs = ['en', 'fr'];
const languagePart = lng.split('-')[0];
if (supportedLngs.includes(languagePart)) {
return languagePart; // Manually return 'en' from 'en-US'
}
return lng;
},
} }
i18n i18n
@@ -22,8 +32,12 @@ i18n
// init i18next // init i18next
// for all options read: https://www.i18next.com/overview/configuration-options // for all options read: https://www.i18next.com/overview/configuration-options
.init({ .init({
fallbackLng: {
'fr-FR': ['fr'],
default: ['en']
},
supportedLngs: ['fr', 'en'], supportedLngs: ['fr', 'en'],
fallbackLng: 'fr', nonExplicitSupportedLngs: true,
debug: vite_url.startsWith('http://localhost'), debug: vite_url.startsWith('http://localhost'),
interpolation: { interpolation: {
escapeValue: false, // not needed for react as it escapes by default escapeValue: false, // not needed for react as it escapes by default

View File

@@ -74,7 +74,7 @@ export function OBSProvider({children}) {
} }
function getElementName(element) { function getElementName(element) {
return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}` return `sub${sessionStorage.getItem("liceName") || 1}.${element}`
} }
export function useOBS() { export function useOBS() {

View File

@@ -23,8 +23,6 @@ export function MemberList({source}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]);
const [showLicenceState, setShowLicenceState] = useState((sessionStorage.getItem("showLicenceState") || "false") === "true");
const setFilter = (filter) => { const setFilter = (filter) => {
navigate("#" + encodeURI(JSON.stringify(filter))) navigate("#" + encodeURI(JSON.stringify(filter)))
@@ -37,7 +35,7 @@ export function MemberList({source}) {
payment: 2, payment: 2,
order: "", order: "",
categorie: "", categorie: "",
archived: false, archived: sessionStorage.getItem("showMembreArchived") || true,
...JSON.parse(decodeURI(hash.substring(1)) || "{}"), ...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
} }
@@ -72,23 +70,11 @@ export function MemberList({source}) {
club: e.club, club: e.club,
categorie: e.categorie, categorie: e.categorie,
licence_number: e.licence, licence_number: e.licence,
licence: showLicenceState ? licenceData.find(licence => licence.membre === e.id) : null licence: data.additionalData?.find(licence => licence.membre === e.id)
}) })
} }
setMemberData(data2); setMemberData(data2);
}, [data, licenceData]); }, [data]);
useEffect(() => {
sessionStorage.setItem("showLicenceState", showLicenceState);
if (!showLicenceState)
return;
toast.promise(
apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
.then(data => {
setLicenceData(data.data);
});
}, [showLicenceState]);
const search = (search) => { const search = (search) => {
if (search === filter.search) if (search === filter.search)
@@ -102,8 +88,8 @@ export function MemberList({source}) {
<div className="col-lg-9"> <div className="col-lg-9">
<SearchBar search={search} defaultValue={filter.search}/> <SearchBar search={search} defaultValue={filter.search}/>
{data {data
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState} ? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} page={filter.page}
page={filter.page} setPage={e => setFilter({...filter, page: e})} source={source}/> setPage={e => setFilter({...filter, page: e})} source={source}/>
: error : error
? <AxiosError error={error}/> ? <AxiosError error={error}/>
: <Def/> : <Def/>
@@ -130,10 +116,11 @@ export function MemberList({source}) {
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">{t('filtre')}</div> <div className="card-header">{t('filtre')}</div>
<div className="card-body"> <div className="card-body">
<FiltreBar showLicenceState={showLicenceState} <FiltreBar showArchived={filter.archived}
setShowLicenceState={setShowLicenceState} setShowArchived={e => {
showArchived={filter.archived} setFilter({...filter, archived: e})
setShowArchived={e => setFilter({...filter, archived: e})} sessionStorage.setItem("showMembreArchived", e);
}}
clubFilter={filter.club} clubFilter={filter.club}
setClubFilter={e => setFilter({...filter, club: e})} setClubFilter={e => setFilter({...filter, club: e})}
source={source} source={source}
@@ -163,6 +150,7 @@ export function MemberList({source}) {
function FileOutput() { function FileOutput() {
const {t} = useTranslation(); const {t} = useTranslation();
function formatColumnDate(worksheet, col) { function formatColumnDate(worksheet, col) {
const range = XLSX.utils.decode_range(worksheet['!ref']) const range = XLSX.utils.decode_range(worksheet['!ref'])
// note: range.s.r + 1 skips the header row // note: range.s.r + 1 skips the header row
@@ -181,6 +169,7 @@ function FileOutput() {
toast.promise( toast.promise(
apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export")) apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export"))
.then(data => { .then(data => {
console.log(data)
const dataOut = [] const dataOut = []
for (const e of data.data) { for (const e of data.data) {
const tmp = { const tmp = {
@@ -354,7 +343,7 @@ function FileInput() {
); );
} }
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) { function MakeCentralPanel({data, visibleMember, navigate, page, setPage, source}) {
const {t} = useTranslation(); const {t} = useTranslation();
const pages = [] const pages = []
@@ -374,7 +363,7 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
})}</small> })}</small>
<div className="list-group"> <div className="list-group">
{visibleMember.map(member => ( {visibleMember.map(member => (
<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))} <MakeRow key={member.id} member={member} navigate={navigate} source={source}/>))}
</div> </div>
</div> </div>
<div className="mb-4"> <div className="mb-4">
@@ -391,11 +380,11 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
</> </>
} }
function MakeRow({member, showLicenceState, navigate, source}) { function MakeRow({member, navigate, source}) {
const rowContent = <> const rowContent = <>
<div className="row" style={{padding: "0.6em 0"}}> <div className="row" style={{padding: "0.6em 0"}}>
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "} <span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "}
{(showLicenceState && member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span> {(member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span>
<div className="ms-2 col-auto"> <div className="ms-2 col-auto">
<div className="fw-bold">{member.fname} {member.lname}</div> <div className="fw-bold">{member.fname} {member.lname}</div>
</div> </div>
@@ -411,7 +400,7 @@ function MakeRow({member, showLicenceState, navigate, source}) {
</> </>
if (showLicenceState && member.licence != null) { if (member.licence != null) {
return <a className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-" return <a className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-"
+ (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))} + (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))}
style={{padding: "0 1em"}} style={{padding: "0 1em"}}
@@ -509,8 +498,6 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
} }
function FiltreBar({ function FiltreBar({
showLicenceState,
setShowLicenceState,
showArchived, showArchived,
setShowArchived, setShowArchived,
clubFilter, clubFilter,
@@ -526,9 +513,6 @@ function FiltreBar({
const {t} = useTranslation(); const {t} = useTranslation();
return <div> return <div>
<div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label={t('membre.filtre.licence')}/>
</div>
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/> <Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/>
</div> </div>

View File

@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons"; import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js"; import {apiAxios, getFirstDateOfSaison, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
@@ -51,13 +51,34 @@ export function LicenceCard({userData}) {
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
}, [data]); }, [data]);
const handleAsk = () => {
const currentLicence = licences[0];
let certif = undefined;
if (currentLicence != null && currentLicence.certificate != null) {
const strings = currentLicence.certificate.split('¤');
if (currentLicence.saison === getSaison()) {
certif = currentLicence.certificate;
} else if (strings.length > 1) {
const date = new Date(strings[1]);
const max = getFirstDateOfSaison();
max.setFullYear(max.getFullYear() - 2);
if (max < date) {
certif = currentLicence.certificate;
}
}
}
setModal({id: -1, membre: userData.id, certificate: certif});
}
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">{t('licence')}</div> <div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal" <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
onClick={_ => setModal({id: -1, membre: userData.id})}>{t('button.ajouter')} onClick={handleAsk}>{t('button.ajouter')}
</button> </button>
</div> </div>
</div> </div>
@@ -141,13 +162,6 @@ function ModalContent({licence, dispatch}) {
if (licence.id !== -1) { if (licence.id !== -1) {
setNew(false) setNew(false)
setSaison(licence.saison) setSaison(licence.saison)
if (licence.certificate === null) {
setCertificateBy("")
setCertificateDate("")
} else {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
}
setValidate(licence.validate) setValidate(licence.validate)
setPay(licence.pay); setPay(licence.pay);
} else { } else {
@@ -158,6 +172,13 @@ function ModalContent({licence, dispatch}) {
setValidate(false) setValidate(false)
setPay(false); setPay(false);
} }
if (licence.certificate) {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
} else {
setCertificateBy("")
setCertificateDate("")
}
}, [licence]); }, [licence]);
return <form onSubmit={e => sendLicence(e, dispatch)}> return <form onSubmit={e => sendLicence(e, dispatch)}>

View File

@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons"; import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js"; import {apiAxios, getFirstDateOfSaison, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {ColoredText} from "../../../components/ColoredCircle.jsx"; import {ColoredText} from "../../../components/ColoredCircle.jsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
@@ -32,7 +32,7 @@ function licenceReducer(licences, action) {
} }
export function LicenceCard({userData}) { export function LicenceCard({userData}) {
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false} const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: null}
const {t} = useTranslation(); const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
@@ -48,13 +48,33 @@ export function LicenceCard({userData}) {
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
}, [data]); }, [data]);
const handleAsk = () => {
const currentLicence = licences[0];
let certif = undefined;
if (currentLicence != null && currentLicence.certificate != null) {
const strings = currentLicence.certificate.split('¤');
if (currentLicence.saison === getSaison()) {
certif = currentLicence.certificate;
} else if (strings.length > 1) {
const date = new Date(strings[1]);
const max = getFirstDateOfSaison();
max.setFullYear(max.getFullYear() - 2);
if (max < date)
certif = currentLicence.certificate;
}
}
setModal({...defaultLicence, certificate: certif});
}
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">{t('licence')}</div> <div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal" <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
onClick={() => setModal(defaultLicence)} onClick={handleAsk}
disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')} disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')}
</button> </button>
</div> </div>
@@ -127,15 +147,14 @@ function ModalContent({licence, dispatch}) {
useEffect(() => { useEffect(() => {
if (licence.id !== -1) { if (licence.id !== -1) {
setNew(false) setNew(false)
if (licence.certificate === null) {
setCertificateBy("")
setCertificateDate("")
} else {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
}
} else { } else {
setNew(true) setNew(true)
}
if (licence.certificate) {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
} else {
setCertificateBy("") setCertificateBy("")
setCertificateDate("") setCertificateDate("")
} }

View File

@@ -1,8 +1,8 @@
import React, {useEffect, useRef, useState} from "react"; import React, {useEffect, useId, useRef, useState} from "react";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {build_tree, resize_tree} from "../../../utils/TreeUtils.js" import {build_tree, from_sendTree, resize_tree} from "../../../utils/TreeUtils.js"
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {CategoryContent} from "./CategoryAdminContent.jsx"; import {CategoryContent} from "./CategoryAdminContent.jsx";
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx"; import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
@@ -14,14 +14,14 @@ import {detectOptimalBackground} from "../../../components/SmartLogoBackground.j
import {faFile, faGlobe, faPrint, faTableCellsLarge, faTrash} from "@fortawesome/free-solid-svg-icons"; import {faFile, faGlobe, faPrint, faTableCellsLarge, faTrash} from "@fortawesome/free-solid-svg-icons";
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import i18n from "i18next"; import i18n from "i18next";
import {getToastMessage} from "../../../utils/Tools.js"; import {getToastMessage, toDataURL, win_end} from "../../../utils/Tools.js";
import {copyStyles} from "../../../utils/copyStyles.js"; import {copyStyles} from "../../../utils/copyStyles.js";
import {StateWindow} from "./StateWindow.jsx"; import {StateWindow} from "./StateWindow.jsx";
import {CombName, useCombs} from "../../../hooks/useComb.jsx"; import {CombName, useCombs} from "../../../hooks/useComb.jsx";
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx"; import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx"; import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx";
import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
import {makePDF} from "../../../utils/cmPdf.jsx"; import {makePDF} from "../../../utils/cmPdf.js";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@@ -52,7 +52,7 @@ export function CMAdmin({compUuid}) {
return <> return <>
<div className="card"> <div className="card">
<div className='card-header'> <div className='card-header'>
<CategoryHeader cat={cat} setCatId={setCatId}/> <CategoryHeader cat={cat} setCatId={setCatId} menuActions={menuActions}/>
</div> </div>
<div className="card-body"> <div className="card-body">
@@ -331,9 +331,8 @@ function Menu({menuActions, compUuid}) {
<strong>{t('config.obs.warn1')}</strong> <strong>{t('config.obs.warn1')}</strong>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">{t('adresseDuServeur')}</span> <span className="input-group-text">{t('adresseDuServeur')}</span>
<span className="input-group-text">{t('config.obs.ws')}</span> <input type="text" className="form-control" placeholder="ws://127.0.0.1:4455/" aria-label=""
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label="" defaultValue={"ws://127.0.0.1:4455/"}/>
defaultValue={"127.0.0.1:4455"}/>
<span className="input-group-text">/</span> <span className="input-group-text">/</span>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
@@ -399,48 +398,146 @@ function Menu({menuActions, compUuid}) {
} }
function PrintModal({menuActions}) { function PrintModal({menuActions}) {
const [categorie, setCategorie] = useState(true); const [categorie, setCategorie] = useState(false);
const [categorieEmpty, setCategorieEmpty] = useState(false); const [categorieEmpty, setCategorieEmpty] = useState(false);
const [preset, setPreset] = useState(false);
const [presetEmpty, setPresetEmpty] = useState(false);
const [allCat, setAllCat] = useState(false);
const [allCatEmpty, setAllCatEmpty] = useState(false);
const [podium, setPodium] = useState(false);
const [podiumRank, setPodiumRank] = useState(4);
const [podiumClub, setPodiumClub] = useState(false);
const {welcomeData} = useWS(); const [presetSelect, setPresetSelect] = useState(-1)
const {sendRequest, welcomeData} = useWS();
const {getComb} = useCombs(); const {getComb} = useCombs();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const podiumPromise = (podiumRank_) => {
return sendRequest("getPodium", {}).then(data => {
return [welcomeData?.name + " - " + t('podium'), [
{type: "podium", params: ({data, maxRank: podiumRank_, minRank: Math.min(4, podiumRank_)})},
]];
});
}
const podiumClubPromise = () => {
return sendRequest("getPodiumClub", {}).then(data => {
return [welcomeData?.name + " - " + t('classementDesClub', {ns: "result"}), [
{type: "podiumClub", params: ({data})},
]];
});
}
const print = (action) => { const print = (action) => {
const pages = []; const pagesPromise = [];
const names = [];
if (categorie) { if (categorie && menuActions.printCategorie)
if (menuActions.printCategorie) { pagesPromise.push(menuActions.printCategorie(categorieEmpty))
const [name, page] = menuActions.printCategorie(categorieEmpty);
pages.push(...page);
names.push(name);
}
}
if (pages.length !== 0) { if (preset && menuActions.printCategoriePreset)
makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t) pagesPromise.push(menuActions.printCategoriePreset(presetEmpty, presetSelect))
}
if (allCat && menuActions.printAllCategorie)
pagesPromise.push(menuActions.printAllCategorie(categorieEmpty, welcomeData?.name + " - " + t('toutesLesCatégories')))
if (podium)
pagesPromise.push(podiumPromise(podiumRank));
if (podiumClub)
pagesPromise.push(podiumClubPromise());
toast.promise(
toDataURL("/Logo-FFSAF-2023.png").then(logo => {
return Promise.allSettled(pagesPromise).then(results => {
const pages = [];
const names = [];
let errors = 0;
for (const result of results) {
if (result.status === "fulfilled") {
const [name, page, error] = result.value;
pages.push(...page);
names.push(name);
errors += error
} else if (result.status === "rejected") {
errors += 1;
}
}
if (errors > 0) {
toast.error(t('erreurGénérationPages', {count: errors}));
}
if (pages.length !== 0) {
makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t, logo)
}
})
}), getToastMessage("toast.print", "cm"))
} }
return <> return <>
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">Quoi imprimer ?</h5> <h5 className="modal-title">{t('quoiImprimer?')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint" <input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint"
onChange={e => setCategorie(e.target.checked)}/> onChange={e => setCategorie(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint">Catégorie sélectionner</label> <label className="form-check-label" htmlFor="checkPrint">{t('catégorieSélectionnée')}</label>
</div> </div>
{categorie && {categorie &&
<div className="form-check" style={{marginLeft: "1em"}}> <div className="form-check" style={{marginLeft: "1em"}}>
<input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2" <input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2"
onChange={e => setCategorieEmpty(e.target.checked)}/> onChange={e => setCategorieEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint2">Feuille vierge</label> <label className="form-check-label" htmlFor="checkPrint2">{t('feuilleVierge')}</label>
</div>} </div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={preset} id="checkPrint3"
onChange={e => setPreset(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint3">{t('touteLaCatégorie')}</label>
</div>
{preset && <div style={{marginLeft: "1em"}}>
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={presetEmpty} id="checkPrint4"
onChange={e => setPresetEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint4">{t('feuilleVierge')}</label>
</div>
<ListPresetSelect value={presetSelect} onChange={setPresetSelect}/>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={allCat} id="checkPrint5"
onChange={e => setAllCat(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint5">{t('toutesLesCatégories')}</label>
</div>
{allCat &&
<div className="form-check" style={{marginLeft: "1em"}}>
<input className="form-check-input" type="checkbox" checked={allCatEmpty} id="checkPrint6"
onChange={e => setAllCatEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint6">{t('feuilleVierge')}</label>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={podium} id="checkPrint7"
onChange={e => setPodium(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint7">{t('podium')}</label>
</div>
{podium &&
<div style={{marginLeft: "1em"}}>
<label htmlFor="range3" className="form-label">{t('jusquauRang')} {podiumRank} </label>
<input type="range" className="form-range" min="1" max="20" step="1" id="range3" value={podiumRank}
onChange={e => setPodiumRank(Number(e.target.value))}/>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={podiumClub} id="checkPrint8"
onChange={e => setPodiumClub(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint8">{t('classementDesClub', {ns: "result"})}</label>
</div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("show")}>{t('afficher')}</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("show")}>{t('afficher')}</button>
@@ -574,9 +671,7 @@ function TeamCardModal() {
</> </>
} }
function CategoryHeader({ function CategoryHeader({cat, setCatId, menuActions}) {
cat, setCatId
}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const bthRef = useRef(null); const bthRef = useRef(null);
const newBthRef = useRef(null); const newBthRef = useRef(null);
@@ -722,10 +817,103 @@ function CategoryHeader({
</div> </div>
</div> </div>
</div> </div>
<PrintCats menuActions={menuActions} cats={cats}/>
</div> </div>
} }
function PrintCats({menuActions, cats}) {
const {cards_v} = useCards();
const {sendRequest} = useWS();
function readAndConvertMatch(matches, data) {
matches.push({
...data,
c1: data.c1?.id,
c2: data.c2?.id,
c1_cacheName: data.c1?.fname + " " + data.c1?.lname,
c2_cacheName: data.c2?.fname + " " + data.c2?.lname
})
}
const run = (categorieEmpty, cats2, name = "") => {
const pagesPromise = cats2.sort((a, b) => a.name.localeCompare(b.name)).map(cat_ => {
return sendRequest('getFullCategory', cat_.id)
.then((data) => {
const cat = {
id: data.id,
name: data.name,
liceName: data.liceName,
type: data.type,
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
raw_trees: data.trees.sort((a, b) => a.level - b.level),
treeAreClassement: data.treeAreClassement,
fullClassement: data.fullClassement,
preset: data.preset,
}
if (name === "") {
name = data.preset.name;
}
const newCards = {};
for (const o of data.cards)
newCards[o.id] = o
let matches2 = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_));
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_));
const activeMatches = matches2.filter(m => m.poule !== '-')
const groups = matches2.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i)
.map(d => {
let poule = activeMatches.find(m => (m.c1 === d || m.c2 === d) && m.categorie_ord !== -42)?.poule
if (!poule)
poule = '-'
return {id: d, poule: poule}
})
matches2 = matches2.filter(m => m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
matches2.forEach(m => {
if (m.end && (!m.scores || m.scores.length === 0))
m.scores = [{n_round: 0, s1: 0, s2: 0}];
})
return {
type: "categorie",
params: ({cat, matches: matches2, groups, cards_v: Object.values({...cards_v, ...newCards}), categorieEmpty})
}
})
})
return Promise.allSettled(pagesPromise)
.then((results) => {
const pages = [];
let error = 0;
for (const result of results) {
if (result.status === "fulfilled") {
pages.push(result.value);
} else {
console.error(result.error);
error++;
}
}
return [name, pages, error];
})
}
menuActions.printCategoriePreset = (categorieEmpty, preset) => {
return run(categorieEmpty, cats.filter(cat => cat.preset?.id === preset))
}
menuActions.printAllCategorie = (categorieEmpty, name) => {
return run(categorieEmpty, cats, name)
}
}
function ModalContent({state, setCatId, setConfirm, confirmRef}) { function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const id = useId()
const [name, setName] = useState("") const [name, setName] = useState("")
const [lice, setLice] = useState("1") const [lice, setLice] = useState("1")
const [poule, setPoule] = useState(true) const [poule, setPoule] = useState(true)
@@ -908,8 +1096,9 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
<ListPresetSelect value={preset} onChange={setPreset}/> <ListPresetSelect value={preset} onChange={setPreset}/>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label> <label htmlFor={id + "liceInput1"} className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice} ';')</small></Trans></label>
<input type="text" className="form-control" id={id + "liceInput1"} placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/> onChange={e => setLice(e.target.value)}/>
</div> </div>

View File

@@ -41,6 +41,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS(); const {dispatch} = useWS();
const publicAffDispatch = usePubAffDispatch();
const {connected, setText} = useOBS(); const {connected, setText} = useOBS();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
@@ -70,6 +71,8 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
useEffect(() => { useEffect(() => {
setText("poule", cat ? cat.name : ""); setText("poule", cat ? cat.name : "");
if (!cat)
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
}, [cat, connected]); }, [cat, connected]);
return <> return <>

View File

@@ -48,7 +48,7 @@ export function PointPanel({menuActions}) {
<div className="col row align-items-center"> <div className="col row align-items-center">
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button> <button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button> { /*<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button> */}
</div> </div>
<SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/> <SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/>
</div> </div>

View File

@@ -160,7 +160,7 @@ function Menu({menuActions}) {
} else { } else {
importOBSConfiguration() importOBSConfiguration()
.then(config => { .then(config => {
connect("ws://" + config.adresse + "/", config.password, config.assets_dir); connect(config.adresse, config.password, config.assets_dir);
}) })
.catch(() => { .catch(() => {
toast.error(t('aucuneConfigurationObs')); toast.error(t('aucuneConfigurationObs'));

View File

@@ -22,7 +22,6 @@ import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.
import {ScorePanel} from "./ScoreAndCardPanel.jsx"; import {ScorePanel} from "./ScoreAndCardPanel.jsx";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {AutoCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx"; import {AutoCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
import {makePDF} from "../../../utils/cmPdf.jsx";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@@ -569,7 +568,9 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
const {active, over} = event; const {active, over} = event;
if (active.id !== over.id) { if (active.id !== over.id) {
const newIndex = marches2.findIndex(m => m.id === over.id); let newIndex = marches2.findIndex(m => m.id === over.id);
if (newIndex > 0)
newIndex = marches2[newIndex].categorie_ord;
reducer({type: 'REORDER', payload: {id: active.id, pos: newIndex}}); reducer({type: 'REORDER', payload: {id: active.id, pos: newIndex}});
sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => { sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => {
}) })

View File

@@ -11,6 +11,7 @@ import {AxiosError} from "../../../components/AxiosError.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {CardsProvider} from "../../../hooks/useCard.jsx"; import {CardsProvider} from "../../../hooks/useCard.jsx";
import AudioEncoder from "../../../components/cm/AudioEncoder.jsx";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@@ -73,6 +74,7 @@ function HomeComp() {
<Route path="/" element={<Home2 perm={perm}/>}/> <Route path="/" element={<Home2 perm={perm}/>}/>
<Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/> <Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/>
<Route path="/table" element={<CMTable/>}/> <Route path="/table" element={<CMTable/>}/>
<Route path="/view/audio" element={<AudioEncoder/>}/>
</Routes> </Routes>
</LoadingProvider> </LoadingProvider>
</CardsProvider> </CardsProvider>

View File

@@ -19,7 +19,7 @@ function reducer(state, action) {
} }
} }
export function StateWindow({document}) { export function useTablesState() {
const {sendRequest, dispatch} = useWS(); const {sendRequest, dispatch} = useWS();
const [state, dispatchState] = useReducer(reducer, []) const [state, dispatchState] = useReducer(reducer, [])
@@ -58,6 +58,12 @@ export function StateWindow({document}) {
} }
}, []) }, [])
return {state};
}
export function StateWindow({document}) {
const {state} = useTablesState();
document.title = "État des tables de marque"; document.title = "État des tables de marque";
document.body.className = "overflow-hidden"; document.body.className = "overflow-hidden";
@@ -66,7 +72,7 @@ export function StateWindow({document}) {
<div className="d-flex flex-row flex-wrap justify-content-around align-items-center align-content-around h-100 p-2 overflow-auto"> <div className="d-flex flex-row flex-wrap justify-content-around align-items-center align-content-around h-100 p-2 overflow-auto">
{state.sort((a, b) => a.liceName.localeCompare(b.liceName)).map((table, index) => {state.sort((a, b) => a.liceName.localeCompare(b.liceName)).map((table, index) =>
<div key={index} className="card d-inline-flex flex-grow-1 align-self-stretch" style={{minWidth: "25em", maxWidth: "30em"}}> <div key={index} className="card d-inline-flex flex-grow-1 align-self-stretch" style={{minWidth: "25em", maxWidth: "30em"}}>
<ShowState table={table} dispatch={dispatchState}/> <ShowState table={table}/>
</div>) </div>)
} }
</div> </div>
@@ -191,7 +197,7 @@ function PrintChrono({chrono}) {
const timer = setInterval(() => { const timer = setInterval(() => {
let currentDuration = chrono.configTime let currentDuration = chrono.configTime
if (chrono.state === 2) { if (chrono.state === 2) {
currentDuration = (chrono.state === 0) ? 10000 : chrono.configPause currentDuration = chrono.configPause
} }
const timeStr = (chrono.state === 1 ? " Match - " : " Pause - ") + timePrint(currentDuration - getTime()) + (isRunning() ? "" : " (arrêté)") const timeStr = (chrono.state === 1 ? " Match - " : " Pause - ") + timePrint(currentDuration - getTime()) + (isRunning() ? "" : " (arrêté)")

View File

@@ -39,7 +39,8 @@ export function ResultView() {
{resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/> {resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/>
|| resultShow && resultShow === "club" && <ClubList uuid={uuid}/> || resultShow && resultShow === "club" && <ClubList uuid={uuid}/>
|| resultShow && resultShow === "comb" && <CombList uuid={uuid}/> || resultShow && resultShow === "comb" && <CombList uuid={uuid}/>
|| resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>} || resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>
|| resultShow && resultShow === "clubs" && <ClubsResult uuid={uuid}/>}
</div> </div>
</div> </div>
</> </>
@@ -66,6 +67,10 @@ function MenuBar({resultShow, setResultShow}) {
<a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")} <a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a> href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a>
</li> </li>
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "clubs" ? " active" : "")} aria-current={(resultShow === "clubs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("clubs")}>{t('classementClub')}</a>
</li>
</ul> </ul>
/* /*
@@ -450,6 +455,43 @@ function CombResult({uuid, combId}) {
</div> </div>
} }
function ClubsResult({uuid}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/club/classement`, setLoading, 1)
const {t} = useTranslation('result');
return <>
{data ? <>
<h3>{t('classementDesClub')} :</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>{t('club')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('1er')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('2eme')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('3eme')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('scores')}</th>
</tr>
</thead>
<tbody>
{data.map((club, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{club.name}</td>
<td style={{textAlign: "center"}}>{club.score[0]}</td>
<td style={{textAlign: "center"}}>{club.score[1]}</td>
<td style={{textAlign: "center"}}>{club.score[2]}</td>
<td style={{textAlign: "center"}}>{club.tt_score}</td>
</tr>)}
</tbody>
</table>
</>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function CombsResult({uuid}) { function CombsResult({uuid}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1) const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1)

View File

@@ -56,8 +56,10 @@ export function MarchReducer(datas, action) {
return datas.sort(action.payload) return datas.sort(action.payload)
case 'REORDER': case 'REORDER':
const oldIndex = datas.findIndex(data => data.id === action.payload.id) const oldIndex = datas.findIndex(data => data.id === action.payload.id)
if (oldIndex === -1 || datas[oldIndex].categorie_ord === action.payload.pos) if (oldIndex === -1)
return datas // Do nothing return datas // Do nothing
if (datas[oldIndex].categorie_ord === action.payload.pos)
return [...datas] // Do nothing
const oldPos = datas[oldIndex].categorie_ord const oldPos = datas[oldIndex].categorie_ord
const newPos = action.payload.pos const newPos = action.payload.pos

View File

@@ -117,6 +117,14 @@ export function getSaison(currentDate = new Date()) {
} }
} }
export function getFirstDateOfSaison() {
const year = getSaison();
const firstDate = new Date(year, 8, 1); // 8 = septembre, 1 = premier jour
firstDate.setHours(0, 0, 0, 0); // Heure, minutes, secondes, millisecondes à 0
return firstDate;
}
export function getCatName(cat) { export function getCatName(cat) {
switch (cat) { switch (cat) {
case "SUPER_MINI": case "SUPER_MINI":
@@ -418,3 +426,29 @@ export function hex2rgb(hex) {
// return {r, g, b} // return {r, g, b}
return {r, g, b}; return {r, g, b};
} }
export function toDataURL(src, outputFormat) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
const canvas = document.createElement('CANVAS');
const ctx = canvas.getContext('2d');
let dataURL;
canvas.height = this.naturalHeight;
canvas.width = this.naturalWidth;
ctx.drawImage(this, 0, 0);
dataURL = canvas.toDataURL(outputFormat);
resolve(dataURL);
};
img.onerror = function() {
reject(new Error('Could not load image at ' + src));
}
img.src = src;
if (img.complete || img.complete === undefined) {
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
img.src = src;
}
});
}

View File

@@ -0,0 +1,502 @@
import {jsPDF} from 'jspdf'
import {useCardsStatic} from "../hooks/useCard.jsx";
import {
CatList,
getCatName,
getShieldSize,
getShieldTypeName,
getSwordSize,
getSwordTypeName,
sortCategories,
timePrint,
virtualScore,
win_end
} from "./Tools.js";
import {getMandatoryProtectionsList} from "../components/ProtectionSelector.jsx";
import {scoreToString2} from "./CompetitionTools.js";
import {TreeNode} from "./TreeUtils.js";
import {drawGraphForPdf} from "../pages/result/DrawGraph.jsx";
import autoTable from 'jspdf-autotable'
function combName(getComb, combId) {
if (!combId)
return " "
const comb = getComb(combId, null);
if (comb) {
if (comb.lname === "__team")
return `${comb.fname}`
return `${comb.fname} ${comb.lname}`
} else {
return `[Comb #${combId}]`
}
}
export function makePDF(action, pagesList, name, c_name, getComb, t, logo) {
//https://github.com/parallax/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L59
const doc = new jsPDF('p', 'pt', 'a4');
doc.setProperties({title: name, author: "FFSAF - Intranet", subject: c_name + " - " + name, creator: "FFSAF - Intranet"});
for (let i = 0; i < pagesList.length; i++) {
const context = {pdf_doc: doc, pdf_name: name, c_name, getComb, t, ...pagesList[i].params, logo}
switch (pagesList[i].type) {
case "categorie":
generateCategoriePDF(context);
break;
case "podium":
generatePodium(context);
break;
case "podiumClub":
generateClubPodium(context);
break;
default:
break
}
if (i !== pagesList.length - 1)
doc.addPage();
}
switch (action) {
case "show":
window.open(doc.output('bloburl', {filename: name + '.pdf'}));
break;
case "print":
const iframe = document.createElement('iframe'); //load content in an iframe to print later
document.body.appendChild(iframe);
iframe.style.display = 'none';
iframe.src = doc.output('bloburl', {filename: name + '.pdf'});
iframe.onload = function () {
setTimeout(function () {
iframe.focus();
iframe.contentWindow.print();
}, 1);
};
break;
case "download":
default:
doc.save(name + '.pdf');
break;
}
}
function matchList(pdf_doc, matches, cat, cards_v, classement, getComb, t) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards_v);
const liceName = (cat.liceName || "N/A").split(";");
const getBG = (combId, match) => {
const c = getHeightCardForCombInMatch(combId, match)
if (!c)
return ""
let bg = "";
let text = "#000";
switch (c.type) {
case "YELLOW":
bg = "#ffc107";
break;
case "RED":
bg = "#dc3545";
text = "#FFF";
break;
case "BLACK":
bg = "#000000";
text = "#FFF";
break;
case "BLUE":
bg = "#0d6efd";
text = "#FFF";
break;
}
return {fillColor: bg, textColor: text}
}
const head = [
...(classement ? [
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
] : [
{content: t('no'), styles: {halign: "center"}},
{content: t('poule'), styles: {halign: "center"}},
{content: t('zone'), styles: {halign: "center"}},
]),
{content: "", styles: {halign: "center"}},
{content: t('rouge'), styles: {halign: "center"}},
{content: t('résultat'), styles: {halign: "center"}},
{content: t('blue'), styles: {halign: "center"}},
{content: "", styles: {halign: "center"}},
]
const body = matches.map((m, index) => ([
...(classement ? [
{content: `${m.categorie_ord + 1}-${m.categorie_ord + 2}`, styles: {halign: "center", padding: "2pt 0"}}
] : [
{content: index + 1, styles: {halign: "center", padding: "2pt 0"}},
{content: m.poule, styles: {halign: "center"}},
{content: liceName[index % liceName.length], styles: {halign: "center"}},
]),
{content: m.end ? (m.win > 0 ? "X" : (m.win === 0 ? "-" : "")) : " ", styles: {halign: "center", ...getBG(m.c1, m)}},
{content: combName(getComb, m.c1), styles: {halign: "center", minWidth: "11em", paddingLeft: "0.2em"}},
{content: scoreToString2(m, cards_v), styles: {halign: "center"}},
{content: combName(getComb, m.c2), styles: {halign: "center", minWidth: "11em", paddingRight: "0.2em"}},
{content: m.end ? (m.win < 0 ? "X" : (m.win === 0 ? "-" : "")) : " ", styles: {halign: "center", ...getBG(m.c2, m)}},
]))
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
columnStyles: classement ? {
0: {cellWidth: 35},
1: {cellWidth: 15},
2: {cellWidth: "auto"},
3: {cellWidth: 110},
4: {cellWidth: "auto"},
5: {cellWidth: 15},
} : {
0: {cellWidth: 20},
1: {cellWidth: 35},
2: {cellWidth: 30},
3: {cellWidth: 15},
4: {cellWidth: "auto"},
5: {cellWidth: 110},
6: {cellWidth: "auto"},
7: {cellWidth: 15},
},
head: [head],
body: body,
theme: 'grid',
})
}
function buildTree(pdf_doc, treeData, treeRaw, matches, cat, cards_v, getComb, t, categorieEmpty, comb_count = 0) {
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = categorieEmpty ? null : getComb(matchData?.c1)
const c2 = categorieEmpty ? null : getComb(matchData?.c2)
const scores2 = []
for (const score of matchData?.scores) {
scores2.push({
...score,
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
})
}
let node = new TreeNode({
...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
return node
}
let out = []
for (let i = 0; i < treeRaw.length; i++) {
if (treeRaw.at(i).level > -10) {
out.push(parseTree(treeData.at(i)))
}
}
const canvas = drawGraphForPdf(out, 24, cards_v)
const imgData = canvas.toDataURL('image/png');
const height = canvas.height * (pdf_doc.internal.pageSize.getWidth() - 80) / canvas.width;
pdf_doc.addImage(imgData, 'PNG', 40, pdf_doc.lastAutoTable.finalY, pdf_doc.internal.pageSize.getWidth() - 80, height);
pdf_doc.lastAutoTable.finalY += height;
if (cat.fullClassement) {
let size = 0;
if (out.length > 0)
size = out[0].getMaxChildrenAtDepth(out[0].death() - 1)
const matches2 = cat.raw_trees?.filter(n => n.level <= -10).reverse().map(d => categorieEmpty ? ({}) : matches.find(m => m.id === d.match?.id))
if (matches2.length === 0) {
while (Math.ceil(comb_count / 2) - size > matches2.length)
matches2.push({})
}
matches2.forEach((v, i) => {
v.categorie_ord = (i + size) * 2
})
matchList(pdf_doc, matches2, cat, cards_v, true, getComb, t)
}
}
function pouleList(pdf_doc, groups, getComb, t) {
const groups2 = groups.map(g => {
const comb = getComb(g.id);
return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
}).sort((a, b) => {
if (a.poule !== b.poule) {
if (a.poule === '-') return 1;
if (b.poule === '-') return -1;
return a.poule.localeCompare(b.poule);
}
return a.name.localeCompare(b.name);
}).reduce((acc, curr) => {
const poule = curr.poule;
if (!acc[poule]) {
acc[poule] = [];
}
acc[poule].push(curr);
return acc;
}, {});
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
head: [Object.keys(groups2).map((poule) => ({content: `${t('poule')} ${poule}`, styles: {halign: "center"}}))],
body: [Object.keys(groups2).map((poule) => ({
content: groups2[poule].map(o => o.name).join(", "),
styles: {halign: "center", valign: "middle"}
}))],
theme: 'grid',
})
}
function makeHeader(pdf_doc, c_name, p_name, logo) {
autoTable(pdf_doc, {
startY: 20,
body: [[
{content: "", src: `${logo}`, rowSpan: 2, styles: {halign: 'center', valign: 'middle'}},
{content: c_name, colSpan: 3, styles: {halign: "center", valign: 'middle', fontSize: 20}},
], [
{content: p_name, colSpan: 3, styles: {halign: "center", valign: 'middle', fontSize: 20}}
]],
theme: 'plain',
columnStyles: {
0: {cellWidth: 60, minCellHeight: 60},
},
didDrawCell: function (data) {
if (data.column.index === 0 && data.row.index === 0) {
const dim = data.cell.height - data.cell.padding('vertical');
const textPos = data.cell;
pdf_doc.addImage(data.cell.raw.src, textPos.x, textPos.y + data.cell.padding('vertical') / 2, dim, dim);
}
}
})
}
function generateCategoriePDF({pdf_doc, cat, matches, groups, getComb, cards_v, c_name, categorieEmpty, t, logo}) {
let catAverage = "---";
let genreAverage = "H";
let nbComb = 0;
let time = 0;
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => categorieEmpty ? ({...m, end: false, scores: []}) : m)
if (marches2.length !== 0) {
const genres = [];
const cats = [];
const combs_ = [];
for (const m of marches2) {
if (m.c1 && !combs_.includes(m.c1))
combs_.push(m.c1);
if (m.c2 && !combs_.includes(m.c2))
combs_.push(m.c2);
}
combs_.map(cId => getComb(cId, null)).filter(c => c && c.categorie)
.forEach(c => {
cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory))
genres.push(c.genre)
});
const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length);
nbComb = combs_.length;
catAverage = CatList.at(catAvg) || "---";
genreAverage = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length) > 0.5 ? "F" : "H";
if (cat.preset && cat.preset.categories) {
const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie));
let p;
if (catAvailable.includes(catAvg)) {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg);
} else {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) ===
catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a));
}
time = {round: p.roundDuration, pause: p.pauseDuration}
}
}
makeHeader(pdf_doc, c_name, cat.name, logo)
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY,
styles: {fontSize: 10, cellPadding: 3},
body: [[
{content: `${t('catégorieDâgeMoyenne')} : ${getCatName(catAverage)}`, styles: {halign: "left"}},
{
content: `${t('arme', {ns: 'common'})} : ${getSwordTypeName(cat.preset?.sword)} - ${t('taille')} ${getSwordSize(cat.preset?.sword, catAverage, genreAverage)}`,
styles: {halign: "left"}
},
{
content: `${t('bouclier', {ns: 'common'})} : ${getShieldTypeName(cat.preset?.shield)} - ${t('taille')} ${getShieldSize(cat.preset?.shield, catAverage)}`,
styles: {halign: "left"}
},
], [
{content: `${t('duréeRound')} : ${timePrint(time.round)}`, styles: {halign: "left"}},
{content: `${t('duréePause')} : ${timePrint(time.pause)}`, styles: {halign: "left"}},
{content: `${t('nombreDeCombattants')} : ${nbComb}`, styles: {halign: "left"}},
], [
{
content: `${t('protectionObligatoire', {ns: 'common'})} ${getMandatoryProtectionsList(CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
cat.preset?.mandatoryProtection1 : cat.preset?.mandatoryProtection2, cat.preset?.shield, t).join(", ") || "---"}`,
colSpan: 3,
styles: {halign: "left"}
},
]],
theme: 'grid',
})
pouleList(pdf_doc, groups, getComb, t)
if ((cat.type & 1) === 1)
matchList(pdf_doc, marches2.filter(m => m.categorie_ord !== -42).sort((a, b) => a.categorie_ord - b.categorie_ord),
cat, categorieEmpty ? [] : cards_v, false, getComb, t)
if ((cat.type & 2) === 2) {
pdf_doc.setFontSize(12);
pdf_doc.text((cat.treeAreClassement ? t('classement') : t('tournois')) + ':', 40, pdf_doc.lastAutoTable.finalY + 16)
pdf_doc.lastAutoTable.finalY += 16;
buildTree(pdf_doc, cat.trees, cat.raw_trees, marches2, cat, categorieEmpty ? [] : cards_v, getComb, t, categorieEmpty, nbComb)
}
}
function generatePodium({pdf_doc, data, t, logo, c_name, minRank = 4, maxRank = 4}) {
makeHeader(pdf_doc, c_name, t('podium'), logo)
const data2 = data.sort((a, b) => {
let tmp = sortCategories(a.categorie, b.categorie);
if (tmp !== 0)
return tmp;
return a.poule_name.localeCompare(b.poule_name)
})
let finalY2 = pdf_doc.lastAutoTable.finalY;
let finalY3 = pdf_doc.lastAutoTable.finalY;
for (let i = 0; i < data2.length; i++) {
const p = data2[i];
let pageNumber = pdf_doc.internal.getNumberOfPages()
const body = Array.from({length: minRank}, () => []);
for (const c of p.podium) {
if (c.rank > maxRank)
continue;
if (body[c.rank - 1])
body[c.rank - 1].push(c.name)
else
body[c.rank - 1] = [c.name]
}
for (let j = 0; j < body.length; j++) {
if (body[j].length === 0)
body[j].push(" ")
body[j] = [
{content: j + 1, styles: {halign: "center"}},
{content: body[j].join(", "), styles: {halign: "center"}}
]
}
autoTable(pdf_doc, {
startY: finalY3 + 7,
margin: i % 2 ? {left: pdf_doc.internal.pageSize.getWidth() / 2 + 7} : {right: pdf_doc.internal.pageSize.getWidth() / 2 + 7},
styles: {fontSize: 10, cellPadding: 3},
columnStyles: {
0: {cellWidth: 35},
1: {cellWidth: "auto"},
},
pageBreak: "avoid",
showHead: 'firstPage',
head: [[
{content: p.poule_name, colSpan: 2, styles: {halign: "center"}},
], [
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
{content: t('combattants', {ns: 'result'}), styles: {halign: "center"}},
]],
body: body,
foot: [[
{content: t('source') + " : " + p.source, colSpan: 2, styles: {halign: "left", fontSize: 8}},
]],
rowPageBreak: 'auto',
theme: 'grid',
})
if (i % 2 === 0) {
finalY2 = pdf_doc.lastAutoTable.finalY;
if (pageNumber !== pdf_doc.internal.getNumberOfPages())
finalY3 = 33
} else {
pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY);
finalY3 = pdf_doc.lastAutoTable.finalY;
}
}
pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY);
}
function generateClubPodium({pdf_doc, data, t, logo, c_name}) {
makeHeader(pdf_doc, c_name, t('podiumDesClubs'), logo)
const body = data.map(c => [
{content: [c.name], styles: {halign: "center"}},
{content: c.score[0], styles: {halign: "center"}},
{content: c.score[1], styles: {halign: "center"}},
{content: c.score[2], styles: {halign: "center"}},
{content: c.tt_score, styles: {halign: "center"}},
]);
let rank = 0;
let lastScores = null;
let lastB = null;
for (const b of body) {
const scores = [b[1].content, b[2].content, b[3].content, b[4].content];
if (lastScores !== scores) {
rank++;
b.unshift({content: rank, styles: {halign: "center"}})
lastB = b
} else {
lastB[1].content.push(b[0].content);
delete body.indexOf(b);
}
}
for (const b of body) {
b[1].content = b[1].content.join(", ")
}
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
columnStyles: {
0: {cellWidth: 35},
1: {cellWidth: "auto"},
2: {cellWidth: 45},
3: {cellWidth: 45},
4: {cellWidth: 45},
5: {cellWidth: 40},
},
pageBreak: "avoid",
showHead: 'firstPage',
head: [[
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
{content: t('club', {ns: 'result'}), styles: {halign: "center"}},
{content: t('1er', {ns: 'result'}), styles: {halign: "center"}},
{content: t('2eme', {ns: 'result'}), styles: {halign: "center"}},
{content: t('3eme', {ns: 'result'}), styles: {halign: "center"}},
{content: t('scores', {ns: 'result'}), styles: {halign: "center"}},
]],
body: body,
rowPageBreak: 'auto',
theme: 'grid',
})
}

View File

@@ -1,378 +0,0 @@
import {jsPDF} from 'jspdf'
import {renderToString} from "react-dom/server";
import React from "react";
import {hasEffectCard, useCardsStatic} from "../hooks/useCard.jsx";
import {CatList, getCatName, getShieldSize, getShieldTypeName, getSwordSize, getSwordTypeName, timePrint, virtualScore, win_end} from "./Tools.js";
import {getMandatoryProtectionsList} from "../components/ProtectionSelector.jsx";
import {scoreToString2} from "./CompetitionTools.js";
import {TreeNode} from "./TreeUtils.js";
import {drawGraphForPdf} from "../pages/result/DrawGraph.jsx";
function CombName({getComb, combId}) {
if (!combId)
return <>&#8195;</>
const comb = getComb(combId, null);
if (comb) {
if (comb.lname === "__team")
return <>{comb.fname}</>
return <>{comb.fname} {comb.lname}</>
} else {
return <>[Comb #{combId}]</>
}
}
function MatchList({matches, cat, cards_v, classement, getComb, t}) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards_v);
const liceName = (cat.liceName || "N/A").split(";");
const getBG = (combId, match, cat) => {
const c = getHeightCardForCombInMatch(combId, match)
if (!c)
return ""
let bg = "";
let text = "#000";
switch (c.type) {
case "YELLOW":
bg = "#ffc107";
break;
case "RED":
bg = "#dc3545";
text = "#FFF";
break;
case "BLACK":
bg = "#000000";
text = "#FFF";
break;
case "BLUE":
bg = "#0d6efd";
text = "#FFF";
break;
}
return {
backgroundColor: bg,
color: text,
borderRadius: c.match === match.id ? "50%" : "0",
opacity: hasEffectCard(c, match.id, cat.id) ? 1 : 0.5
}
}
return <table style={{width: "100%", borderCollapse: "collapse"}} border={1}>
<thead>
<tr>
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{t('no')}</th>}
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{t('poule')}</th>}
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{t('zone')}</th>}
<th></th>
<th style={{textAlign: "center"}}>{t('rouge')}</th>
<th style={{textAlign: "center"}}>{t('résultat')}</th>
<th style={{textAlign: "center"}}>{t('blue')}</th>
<th></th>
</tr>
</thead>
<tbody>
{matches.map((m, index) => (
<tr key={m.id} id={m.id} style={{
background: index % 2 ? "#ededed" : "white",
borderLeft: "0.5px solid gray",
borderTop: index === 0 ? "2px solid black" : ""
}}>
{!classement && <th style={{textAlign: "center", padding: "2pt 0"}} scope="row">{index + 1}</th>}
{!classement && <td style={{textAlign: "center"}}>{m.poule}</td>}
{!classement && <td style={{textAlign: "center"}}>{liceName[index % liceName.length]}</td>}
<td style={{textAlign: "center", ...getBG(m.c1, m, cat)}}>{m.end && ((m.win > 0 && "X") || (m.win === 0 && "-"))}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}><CombName combId={m.c1} getComb={getComb}/></td>
<td style={{textAlign: "center"}}>{scoreToString2(m, cards_v)}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}><CombName combId={m.c2} getComb={getComb}/></td>
<td style={{textAlign: "center", ...getBG(m.c2, m, cat)}}>{m.end && ((m.win < 0 && "X") || (m.win === 0 && "-"))}</td>
</tr>
))}
</tbody>
</table>
}
function BuildTree({treeData, treeRaw, matches, cat, cards_v, getComb, t, categorieEmpty}) {
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = categorieEmpty ? null : getComb(matchData?.c1)
const c2 = categorieEmpty ? null : getComb(matchData?.c2)
const scores2 = []
for (const score of matchData?.scores) {
scores2.push({
...score,
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
})
}
let node = new TreeNode({
...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
return node
}
function initTree(data_in, data_raw) {
let out = []
for (let i = 0; i < data_raw.length; i++) {
if (data_raw.at(i).level > -10) {
out.push(parseTree(data_in.at(i)))
}
}
return out
}
const imgData = drawGraphForPdf(initTree(treeData, treeRaw), 24, cards_v).toDataURL('image/png');
return <div>
<img src={imgData} alt="Arbre" style={{width: "100%", margin: "4pt 0", objectFit: "scale-down"}}/>
{cat.fullClassement &&
<MatchList
matches={cat.raw_trees?.filter(n => n.level <= -10).reverse().map(d => categorieEmpty ? ({}) : matches.find(m => m.id === d.match?.id))}
cat={cat}
cards_v={cards_v} classement={true} t={t} getComb={getComb}/>}
</div>
}
function PouleList({groups, getComb, t}) {
const groups2 = groups.map(g => {
const comb = getComb(g.id);
return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
}).sort((a, b) => {
if (a.poule !== b.poule) {
if (a.poule === '-') return 1;
if (b.poule === '-') return -1;
return a.poule.localeCompare(b.poule);
}
return a.name.localeCompare(b.name);
}).reduce((acc, curr) => {
const poule = curr.poule;
if (!acc[poule]) {
acc[poule] = [];
}
acc[poule].push(curr);
return acc;
}, {});
return <table style={{width: "100%"}}>
<thead style={{borderCollapse: "collapse"}}>
<tr style={{borderCollapse: "collapse"}}>
{Object.keys(groups2).map((poule) => <th key={poule} style={{
textAlign: "center",
borderCollapse: "collapse",
border: "1px solid black"
}}>{t('poule')} {poule}</th>)}
</tr>
</thead>
<tbody style={{borderCollapse: "collapse"}}>
<tr style={{borderCollapse: "collapse"}}>
{Object.keys(groups2).map((poule) => <td key={poule} style={{
textAlign: "center",
borderCollapse: "collapse",
border: "1px solid black"
}}>{groups2[poule].map(o => o.name).join(", ")}</td>)}
</tr>
</tbody>
</table>
}
export function makePDF(action, pagesList, name, c_name, getComb, t) {
//https://github.com/parallax/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L59
const doc = new jsPDF('p', 'pt', 'a4');
let pageHeight = doc.internal.pageSize.getHeight();
htmlPage(doc, pagesList);
function htmlPage(doc2, pages, index = 0) {
const hasPages = pages.length > 0;
if (!hasPages) {
doc2.setProperties({title: name, author: "FFSAF - Intranet", subject: c_name + " - " + name, creator: "FFSAF - Intranet"});
switch (action) {
case "show":
window.open(doc.output('bloburl', {filename: name + '.pdf'}));
break;
case "print":
const iframe = document.createElement('iframe'); //load content in an iframe to print later
document.body.appendChild(iframe);
iframe.style.display = 'none';
iframe.src = doc.output('bloburl', {filename: name + '.pdf'});
iframe.onload = function () {
setTimeout(function () {
iframe.focus();
iframe.contentWindow.print();
}, 1);
};
break;
case "download":
default:
doc.save(name + '.pdf');
break;
}
return;
}
let htmlElement = renderToString(
<div style={{width: "835.82px", margin: "0 auto", letterSpacing: "0.065em"}}>
{processPage(pages[0], ({pdf_name: name, c_name, getComb, t}))}
</div>);
return doc2.html(htmlElement, {
y: index * pageHeight,
callback: function (pdf) {
if (index > 0 && pages.length > 1) pdf.addPage("a4", "p");
const restPages = pages.slice();
restPages.splice(0, 1);
htmlPage(pdf, restPages, index + 1);
},
html2canvas: {
scale: 0.65,
logging: false,
letterRendering: 1,
allowTaint: true,
useCORS: true,
},
margin: [30, 30, 30, 30],
width: 595.28,
//windowWidth: 300
});
}
}
function processPage(page, context) {
switch (page.type) {
case "categorie":
return <GenerateCategoriePDF {...context} {...page.params} />
default:
return <></>
}
}
export function GenerateCategoriePDF({cat, matches, groups, getComb, cards_v, c_name, categorieEmpty, t}) {
let catAverage = "---";
let genreAverage = "H";
let nbComb = 0;
let time = 0;
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => categorieEmpty ? ({...m, end: false, scores: []}) : m)
if (marches2.length !== 0) {
const genres = [];
const cats = [];
const combs_ = [];
for (const m of marches2) {
if (m.c1 && !combs_.includes(m.c1))
combs_.push(m.c1);
if (m.c2 && !combs_.includes(m.c2))
combs_.push(m.c2);
}
combs_.map(cId => getComb(cId, null)).filter(c => c && c.categorie)
.forEach(c => {
cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory))
genres.push(c.genre)
});
const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length);
nbComb = combs_.length;
catAverage = CatList.at(catAvg) || "---";
genreAverage = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length) > 0.5 ? "F" : "H";
if (cat.preset && cat.preset.categories) {
const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie));
let p;
if (catAvailable.includes(catAvg)) {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg);
} else {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) ===
catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a));
}
time = {round: p.roundDuration, pause: p.pauseDuration}
}
}
return <>
<table style={{width: "100%", borderCollapse: "collapse", border: "1px solid black"}}>
<thead>
<tr>
<td>
<table style={{width: "100%"}}>
<tbody>
<tr>
<td style={{width: "90pt"}}>
<img src="/Logo-FFSAF-2023.png" alt="Logo" style={{height: "90pt", width: "90pt"}}/>
</td>
<td align={'center'}>
<h2 style={{marginLeft: "10pt", textAlign: "center"}}>{c_name}</h2>
<h2 style={{marginLeft: "10pt", textAlign: "center"}}>{cat.name}</h2>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</thead>
<tbody className="table-group-divider">
<tr>
<td>
<table style={{width: "100%"}}>
<tbody>
<tr>
<td><strong>{t('catégorieDâgeMoyenne')} :</strong> {getCatName(catAverage)}</td>
<td>
<strong>{t('arme', {ns: 'common'})} :</strong> {getSwordTypeName(cat.preset?.sword)} - {t('taille')} {getSwordSize(cat.preset?.sword, catAverage, genreAverage)}
</td>
<td>
<strong>{t('bouclier', {ns: 'common'})} :</strong> {getShieldTypeName(cat.preset?.shield)} - {t('taille')} {getShieldSize(cat.preset?.shield, catAverage)}
</td>
</tr>
<tr>
<td><strong>{t('duréeRound')} :</strong> {timePrint(time.round)}</td>
<td><strong>{t('duréePause')} :</strong> {timePrint(time.pause)}</td>
<td><strong>{t('nombreDeCombattants')} :</strong> {nbComb}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<strong>{t('protectionObligatoire', {ns: 'common'})}</strong> {getMandatoryProtectionsList(CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
cat.preset?.mandatoryProtection1 : cat.preset?.mandatoryProtection2, cat.preset?.shield, t).join(", ") || "---"}
</td>
</tr>
</tfoot>
</table>
<div style={{marginTop: "10pt"}}>
<PouleList groups={groups} t={t} getComb={getComb}/>
</div>
{(cat.type & 1) === 1 &&
<div style={{marginTop: "7pt"}}>
<MatchList matches={marches2.filter(m => m.categorie_ord !== -42).sort((a, b) => a.categorie_ord - b.categorie_ord)} cat={cat}
cards_v={categorieEmpty ? [] : cards_v} classement={false} t={t} getComb={getComb}/>
</div>}
{(cat.type & 2) === 2 &&
<div style={{marginTop: "7pt"}}>
<strong>{cat.treeAreClassement ? t('classement') : t('tournois')} :</strong>
<BuildTree treeData={cat.trees} treeRaw={cat.raw_trees} cat={cat} matches={marches2} cards_v={categorieEmpty ? [] : cards_v}
getComb={getComb} t={t} categorieEmpty={categorieEmpty}/>
</div>}
</>
}