Compare commits

...

10 Commits

Author SHA1 Message Date
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
752f03cba5 feat: PDF for categories 2026-02-16 22:43:14 +01:00
d857fce71f feat: support decimal on weight 2026-02-13 21:42:48 +01:00
2fd09af0ea feat: auto categories full competition 2026-02-13 21:33:19 +01:00
d43cdc1a4e feat: auto categories 2026-02-11 14:08:13 +01:00
8663aa61cf feat: add silver cup png to cm 2026-02-06 18:02:11 +01:00
758e02dc5b feat: remove ^/api$ path log 2026-02-06 17:26:33 +01:00
33 changed files with 2282 additions and 145 deletions

View File

@ -44,7 +44,9 @@ public class FrontendForwardingFilter implements ContainerResponseFilter {
final String path = info.getPath();
final String address = request.remoteAddress().toString();
if (!path.equals("/api")) {
LOG.infof("Request %s %s from IP %s", method, path, address);
}
int status = responseContext.getStatus();
if (status != 404 && !(status == 405 && "GET".equals(requestContext.getMethod()))) {

View File

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

View File

@ -43,8 +43,8 @@ public class CompetitionGuestModel implements CombModel {
String country = "fr";
Integer weight = null;
Integer weightReal = null;
Float weight = null;
Float weightReal = null;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable(
@ -109,7 +109,7 @@ public class CompetitionGuestModel implements CombModel {
return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_));
}
public Integer getWeight2() {
public Float getWeight2() {
return (this.weightReal != null) ? this.weightReal : this.weight;
}
}

View File

@ -37,8 +37,8 @@ public class RegisterModel {
@JoinColumn(name = "id_membre")
MembreModel membre;
Integer weight;
Integer weightReal;
Float weight;
Float weightReal;
int overCategory = 0;
Categorie categorie;
@ -61,7 +61,7 @@ public class RegisterModel {
)
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
public RegisterModel(CompetitionModel competition, MembreModel membre, Integer weight, int overCategory,
public RegisterModel(CompetitionModel competition, MembreModel membre, Float weight, int overCategory,
Categorie categorie, ClubModel club) {
this.id = new RegisterId(competition.getId(), membre.getId());
this.competition = competition;
@ -91,7 +91,7 @@ public class RegisterModel {
return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)];
}
public Integer getWeight2() {
public Float getWeight2() {
if (weightReal != null)
return weightReal;
return weight;

View File

@ -27,7 +27,7 @@ public class CombEntity {
Genre genre;
String country;
int overCategory;
Integer weight;
Float weight;
List<CombEntity> teamMembers;
List<Long> categoriesInscrites;
@ -47,7 +47,8 @@ public class CombEntity {
return null;
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null,
model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight(),
model.getClub(), model.getGenre(), model.getCountry(), 0,
model.getWeight2() != null ? model.getWeight2() : model.getWeight(),
Stream.concat(model.getComb().stream().map(CombEntity::fromModel),
model.getGuest().stream().map(CombEntity::fromModel)).toList(),
new ArrayList<>());
@ -68,7 +69,8 @@ public class CombEntity {
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(),
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight(), new ArrayList<>(),
new ArrayList<>());
model.getCountry(), registerModel.getOverCategory(),
registerModel.getWeight2() != null ? registerModel.getWeight2() : registerModel.getWeight(),
new ArrayList<>(), new ArrayList<>());
}
}

View File

@ -368,6 +368,9 @@ public class CompetitionService {
return Panache.withTransaction(() -> repository.persist(c));
})
.chain(combModel -> updateRegister(data, c, combModel, true, false)))
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
.voidItem())
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
} else {
@ -406,8 +409,8 @@ public class CompetitionService {
}))
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(model.getCompetition().getUuid(),
r) : Uni.createFrom().voidItem()))
sRegister.sendRegisterNoFetch(model.getCompetition().getUuid(), r)
: Uni.createFrom().voidItem()))
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
}
if ("club".equals(source))
@ -428,6 +431,9 @@ public class CompetitionService {
throw new DForbiddenException(trad.t("insc.err1"));
}))
.chain(combModel -> updateRegister(data, c, combModel, false, false)))
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
.voidItem())
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
@ -444,6 +450,8 @@ public class CompetitionService {
throw new DForbiddenException(trad.t("insc.err2"));
}))
.chain(combModel -> updateRegister(data, c, combModel, false, false)))
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom().voidItem())
.map(r -> SimpleRegisterComb.fromModel(r, List.of()).setCategorieInscrite(r.getCategoriesInscrites()));
}
@ -453,20 +461,21 @@ public class CompetitionService {
if (!"admin".equals(source))
return Uni.createFrom().failure(new DForbiddenException());
return Multi.createFrom().iterable(datas).onItem().transformToUni(data ->
makeImportUpdate(securityCtx, id, data).onFailure().recoverWithItem(t -> {
return permService.hasEditPerm(securityCtx, id)
.chain(cm -> Multi.createFrom().iterable(datas).onItem().transformToUni(data ->
makeImportUpdate(cm, data).onFailure().recoverWithItem(t -> {
SimpleRegisterComb errorComb = new SimpleRegisterComb();
errorComb.setLicence(-42);
errorComb.setFname("ERROR");
errorComb.setLname(t.getMessage());
return errorComb;
})).concatenate().collect().asList();
})).concatenate().collect().asList());
}
private Uni<SimpleRegisterComb> makeImportUpdate(SecurityCtx securityCtx, Long id, RegisterRequestData data) {
@WithSession
public Uni<SimpleRegisterComb> makeImportUpdate(CompetitionModel c, RegisterRequestData data) {
if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
return permService.hasEditPerm(securityCtx, id)
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
return findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
.call(combModel -> {
if (c.getBanMembre() == null)
@ -474,12 +483,11 @@ public class CompetitionService {
c.getBanMembre().remove(combModel.getId());
return Panache.withTransaction(() -> repository.persist(c));
})
.chain(combModel -> updateRegister(data, c, combModel, true, true)))
.chain(combModel -> updateRegister(data, c, combModel, true, true))
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
} else {
return permService.hasEditPerm(securityCtx, id)
.chain(c -> findGuestOrInit(data.getFname(), data.getLname(), c))
return findGuestOrInit(data.getFname(), data.getLname(), c)
.invoke(Unchecked.consumer(model -> {
if (data.getCategorie() == null)
throw new DBadRequestException(trad.t("categorie.requise"));
@ -503,13 +511,13 @@ public class CompetitionService {
} else
model.setCountry(data.getCountry());
if (model.getCompetition().getRequiredWeight().contains(model.getCategorie())) {
if (c.getRequiredWeight().contains(model.getCategorie())) {
if (data.getCountry() != null)
model.setWeight(data.getWeight());
}
}))
.call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(),
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", c,
data.getCategoriesInscrites())
.invoke(cats -> {
g.getCategoriesInscrites().clear();
@ -518,9 +526,8 @@ public class CompetitionService {
.noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
}))
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(model.getCompetition().getUuid(),
r) : Uni.createFrom().voidItem()))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()))
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
}
}

View File

@ -188,6 +188,7 @@ public class ResultService {
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w,
stat.pointMake, stat.pointTake, stat.getPointRate());
})
.filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0)
.sorted(Comparator
.comparing(ResultCategoryData.RankArray::getScore)
.thenComparing(ResultCategoryData.RankArray::getWin)
@ -212,7 +213,7 @@ 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) {
if ((categoryModel.getType() & 2) != 0) {
AtomicInteger rank = new AtomicInteger(0);
@ -258,7 +259,7 @@ public class ResultService {
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
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)));
}
} else {

View File

@ -107,7 +107,7 @@ public class CompetitionData {
public static class SimpleRegister {
long id;
int overCategory;
Integer weight;
Float weight;
Categorie categorie;
Long club;
String club_str;

View File

@ -18,8 +18,8 @@ public class RegisterRequestData {
private String fname;
private String lname;
private Integer weight;
private Integer weightReal;
private Float weight;
private Float weightReal;
private Integer overCategory;
private boolean lockEdit = false;
private List<Long> categoriesInscrites;

View File

@ -26,8 +26,8 @@ public class SimpleRegisterComb {
private Categorie categorie;
private SimpleClubModel club;
private Integer licence;
private Integer weight;
private Integer weightReal;
private Float weight;
private Float weightReal;
private int overCategory;
private boolean hasLicenceActive;
private boolean lockEdit;

View File

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

View File

@ -29,6 +29,7 @@ import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@WithSession
@ -117,16 +118,52 @@ public class RCategorie {
categoryModel.setTree(new ArrayList<>());
categoryModel.setType(categorie.type);
categoryModel.setLiceName(categorie.liceName);
categoryModel.setTreeAreClassement(categorie.treeAreClassement);
categoryModel.setFullClassement(categorie.fullClassement);
if (categorie.preset() != null)
return catPresetRepository.findById(categorie.preset().getId())
.invoke(categoryModel::setPreset)
.chain(__ -> categoryRepository.create(categoryModel));
return categoryRepository.create(categoryModel);
})
.invoke(cat -> SSCategorie.sendAddCategory(connection, cat))
.map(CategoryModel::getId);
}
@WSReceiver(code = "createOrReplaceCategory", permission = PermLevel.ADMIN)
public Uni<Long> createOrReplaceCategory(WebSocketConnection connection, JustCategorie categorie) {
return matchRepository.list("category.compet.uuid = ?1 AND category.name = ?2", connection.pathParam("uuid"),
categorie.name)
.chain(existing -> {
if (existing.isEmpty())
return createCategory(connection, categorie);
Map<Long, List<MatchModel>> matchesByCategory = existing.stream()
.filter(m -> m.getCategory() != null)
.collect(Collectors.groupingBy(m -> m.getCategory().getId()));
for (Map.Entry<Long, List<MatchModel>> entry : matchesByCategory.entrySet()) {
Long categoryId = entry.getKey();
List<MatchModel> matches = entry.getValue();
if (matches.stream().noneMatch(m -> !m.getScores().isEmpty() || m.isEnd()))
return Panache.withTransaction(() -> updateCategory(connection, categorie, categoryId)
.call(__ -> treeRepository.delete("category = ?1", categoryId))
.call(__ -> matchRepository.delete("category.id = ?1", categoryId)))
.replaceWith(categoryId);
}
return createCategory(connection, categorie);
});
}
@WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN)
public Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie) {
return getById(categorie.id, connection)
return updateCategory(connection, categorie, categorie.id);
}
private Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie, Long id) {
return getById(id, connection)
.call(cat -> {
if (categorie.preset() == null) {
cat.setPreset(null);

View File

@ -0,0 +1,107 @@
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();
});
}
@RegisterForReflection
public static record PodiumEntity(String poule_name, String source, Categorie categorie,
List<ResultCategoryData.ClassementData> podium) {
}
}

View File

@ -86,7 +86,7 @@ public class RTeam {
.max(Integer::compareTo)
.map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1));
List<Integer> s = Stream.concat(
List<Float> s = Stream.concat(
pair.getKey().stream().map(RegisterModel::getWeight),
pair.getValue().stream().map(CompetitionGuestModel::getWeight))
.filter(Objects::nonNull).toList();
@ -95,7 +95,7 @@ public class RTeam {
} else if (s.size() == 1) {
team.setWeight(s.get(0));
} else {
team.setWeight((int) s.stream().mapToInt(Integer::intValue)
team.setWeight((float) s.stream().mapToDouble(Float::doubleValue)
.average()
.orElse(0));
}

View File

@ -40,6 +40,16 @@ public class SRegister {
.chain(cardModels -> send(uuid, "sendCards", cardModels))));
}
public Uni<Void> sendRegisterNoFetch(String uuid, RegisterModel registerModel) {
return send(uuid, "sendRegister",
CombEntity.fromModel(registerModel).addCategoriesInscrites(registerModel.getCategoriesInscrites()))
.call(__ -> registerModel.getClub2() == null ? Uni.createFrom().voidItem() :
cardService.addTeamCartToNewComb(registerModel.getMembre().getId(),
registerModel.getClub2().getClubId(), registerModel.getClub2().getName(),
registerModel.getCompetition())
.chain(cardModels -> send(uuid, "sendCards", cardModels)));
}
public Uni<Void> sendRegister(String uuid, CompetitionGuestModel model) {
return Mutiny.fetch(model.getCategoriesInscrites()).chain(o ->
send(uuid, "sendRegister", CombEntity.fromModel(model).addCategoriesInscrites(o))
@ -48,6 +58,14 @@ public class SRegister {
.chain(cardModels -> send(uuid, "sendCards", cardModels))));
}
public Uni<Void> sendRegisterNoFetch(String uuid, CompetitionGuestModel model) {
return send(uuid, "sendRegister",
CombEntity.fromModel(model).addCategoriesInscrites(model.getCategoriesInscrites()))
.call(__ -> cardService.addTeamCartToNewComb(model.getId() * -1,
null, model.getClub(), model.getCompetition())
.chain(cardModels -> send(uuid, "sendCards", cardModels)));
}
public Uni<Void> sendRegisterRemove(String uuid, Long combId) {
return send(uuid, "sendRegisterRemove", combId)
.call(__ -> cardService.rmTeamCardFromComb(combId, uuid));

View File

@ -89,3 +89,6 @@ carton.non.trouver=Card not found
card.cannot.be.added=Unable to add the card
configuration.non.supportee=Unsupported configuration
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
configuration.non.supportee=Configuration non supportée
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

View File

@ -21,6 +21,8 @@
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jspdf": "^4.1.0",
"jspdf-autotable": "^5.0.7",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7",
@ -1605,6 +1607,19 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "19.2.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
@ -1631,6 +1646,13 @@
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz",
"integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA=="
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -1884,6 +1906,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
@ -2028,6 +2060,26 @@
}
]
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
@ -2088,6 +2140,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -2139,6 +2203,16 @@
"node": ">=4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
@ -2384,6 +2458,16 @@
"node": ">=0.4.0"
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -2952,6 +3036,23 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-png/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@ -3348,6 +3449,20 @@
"void-elements": "3.1.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/i18next": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz",
@ -3471,6 +3586,12 @@
"node": ">=12"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -3923,6 +4044,38 @@
"node": ">=6"
}
},
"node_modules/jspdf": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz",
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz",
"integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3 || ^4"
}
},
"node_modules/jspdf/node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@ -4358,6 +4511,13 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -4485,6 +4645,16 @@
"node": ">=6"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
@ -4732,6 +4902,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -4783,6 +4960,16 @@
"node": ">=4"
}
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
@ -5071,6 +5258,16 @@
"node": ">=0.8"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@ -5246,6 +5443,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -5438,6 +5655,16 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uzip": {
"version": "0.20201231.0",
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",

View File

@ -23,6 +23,8 @@
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jspdf": "^4.1.0",
"jspdf-autotable": "^5.0.7",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7",

View File

@ -5,6 +5,7 @@
"actuel": "Current",
"administration": "Administration",
"adresseDuServeur": "Server address",
"afficher": "Show",
"ajoutAutomatique": "Automatic addition",
"ajouter": "Add",
"ajouterDesCombattants": "Add fighters",
@ -23,7 +24,11 @@
"cartonNoir": "Black card",
"cartonRouge": "Red card",
"catégorie": "Category",
"catégorieDâgeMoyenne": "Middle-aged category",
"catégorieSélectionnée": "Selected category",
"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?",
"certainsCombattantsNontPasDePoidsRenseigné": "Some fighters do not have a weight listed; they will NOT be included in the categories.",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
@ -58,11 +63,15 @@
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
"contre": "vs",
"couleur": "Color",
"créationDeLaLesCatégories": "Creating the category(ies)",
"créerLaPhaseFinaleSilYADesPoules": "Create the final phase if there are groups.",
"créerLesMatchesDeClassement": "Create the ranking matches",
"créerLesMatchesDeClassement.msg": "Ranking matches have already been set up/played; recreating these matches will delete them all (you will therefore lose any results). Please note down any information you wish to keep.",
"créerLesMatchs": "Create matches",
"créerToutesLesCatégories": "Create all categories",
"date": "Date",
"demi-finalesEtFinales": "Semi-finals and finals",
"depuisUneCatégoriePrédéfinie": "From a predefined category",
"duréePause": "Pause duration",
"duréeRound": "Round duration",
"editionDeLaCatégorie": "Edit category",
@ -78,21 +87,26 @@
"etatDesTablesDeMarque": "State of marque tables",
"exporter": "Export",
"fermer": "Close",
"feuilleVierge": "Blank sheet",
"finalesUniquement": "Finals only",
"genre": "Gender",
"genre.f": "F",
"genre.h": "M",
"genre.na": "NA",
"imprimer": "Print",
"individuelle": "Individual",
"informationCatégorie": "Category information",
"inscrit": "Registered",
"jusquauRang": "Up to the rank",
"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.",
"lesCombattantsEnDehors2": "Fighters outside the ranking tournament will have a ranking match",
"listeDesCartons": "List of cards",
"manche": "Round",
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
"matchTerminé": "Match over",
"matches": "Matches",
"modeDeCréation": "Creation method",
"modifier": "Edit",
"msg1": "There are already matches in this pool; what do you want to do with them?",
"neRienConserver": "Keep nothing",
@ -105,10 +119,12 @@
"nouvelle...": "New...",
"obs.préfixDesSources": "Source prefix",
"pays": "Country",
"personnaliser": "Personalize",
"poids": "Weight",
"poule": "Pool",
"poulePour": "Pool for: ",
"préparation...": "Preparing...",
"quoiImprimer?": "What print?",
"remplacer": "Replace",
"rouge": "Red",
"réinitialiser": "Reset",
@ -128,6 +144,7 @@
"select.sélectionnerDesCombatants": "Select fighters",
"select.à": "to",
"serveur": "Server",
"source": "Source",
"suivant": "Next",
"supprimer": "Delete",
"supprimerUn": "Delete one",
@ -152,6 +169,9 @@
"toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...",
"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.pending": "Updating team...",
"toast.team.update.success": "Team updated!",
@ -171,6 +191,8 @@
"tournois": "Tournaments",
"tousLesMatchs": "All matches",
"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.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",

View File

@ -5,6 +5,7 @@
"actuel": "Actuel",
"administration": "Administration",
"adresseDuServeur": "Adresse du serveur",
"afficher": "Afficher",
"ajoutAutomatique": "Ajout automatique",
"ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants",
@ -23,7 +24,11 @@
"cartonNoir": "Carton noir",
"cartonRouge": "Carton rouge",
"catégorie": "Catégorie",
"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",
"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",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
@ -38,7 +43,7 @@
"chronomètre": "Chronomètre",
"classement": "Classement",
"club": "Club",
"combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélections ci-dessus.",
"combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélections ci-dessus.",
"compétition": "Compétition",
"compétitionManager": "Compétition manager",
"config.obs.dossierDesResources": "Dossier des resources",
@ -58,11 +63,15 @@
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
"contre": "contre",
"couleur": "Couleur",
"créationDeLaLesCatégories": "Création de la/les catégories",
"créerLaPhaseFinaleSilYADesPoules": "Créer la phase finale s'il y a des poules",
"créerLesMatchesDeClassement": "Créer les matches de classement",
"créerLesMatchesDeClassement.msg": "Des matches de classement ont déjà été configurer/jouer, la recréation de ces matches vont tous les supprimer (vous perdre donc les résultats s'il y en a). Mercie de noter de votre côté les informations que vous voulez conserver.",
"créerLesMatchs": "Créer les matchs",
"créerToutesLesCatégories": "Créer toutes les catégories",
"date": "Date",
"demi-finalesEtFinales": "Demi-finales et finales",
"depuisUneCatégoriePrédéfinie": "Depuis une catégorie prédéfinie",
"duréePause": "Durée pause",
"duréeRound": "Durée round",
"editionDeLaCatégorie": "Edition de la catégorie",
@ -78,21 +87,26 @@
"etatDesTablesDeMarque": "Etat des tables de marque",
"exporter": "Exporter",
"fermer": "Fermer",
"feuilleVierge": "Feuille vierge",
"finalesUniquement": "Finales uniquement",
"genre": "Genre",
"genre.f": "F",
"genre.h": "H",
"genre.na": "NA",
"imprimer": "Imprimer",
"individuelle": "Individuelle",
"informationCatégorie": "Information catégorie",
"inscrit": "Inscrit",
"jusquauRang": "Jusqu'au rang",
"leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules",
"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",
"listeDesCartons": "Liste des cartons",
"manche": "Manche",
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
"matchTerminé": "Match terminé",
"matches": "Matches",
"modeDeCréation": "Mode de création",
"modifier": "Modifier",
"msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?",
"neRienConserver": "Ne rien conserver",
@ -105,10 +119,12 @@
"nouvelle...": "Nouvelle...",
"obs.préfixDesSources": "Préfix des sources",
"pays": "Pays",
"personnaliser": "Personnaliser",
"poids": "Poids",
"poule": "Poule",
"poulePour": "Poule pour: ",
"préparation...": "Préparation...",
"quoiImprimer?": "Quoi imprimer ?",
"remplacer": "Remplacer",
"rouge": "Rouge",
"réinitialiser": "Réinitialiser",
@ -128,6 +144,7 @@
"select.sélectionnerDesCombatants": "Sélectionner des combatants",
"select.à": "à",
"serveur": "Serveur",
"source": "Source",
"suivant": "Suivant",
"supprimer": "Supprimer",
"supprimerUn": "Supprimer un",
@ -152,6 +169,9 @@
"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.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.pending": "Mise à jour de l'équipe...",
"toast.team.update.success": "Équipe mise à jour !",
@ -171,6 +191,8 @@
"tournois": "Tournois",
"tousLesMatchs": "Tous les matchs",
"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.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",

View File

@ -164,3 +164,27 @@ const ProtectionSelector = ({
}
export default ProtectionSelector;
export function getMandatoryProtectionsList(mandatoryProtection, shield, t) {
const protections = [];
const isOn = (bit) => (mandatoryProtection & (1 << (bit - 1))) !== 0;
if (isOn(1)) protections.push(t('casque', {ns: "common"}));
if (isOn(2)) protections.push(t('gorgerin', {ns: "common"}));
if (isOn(3)) protections.push(t('coquilleProtectionPelvienne', {ns: "common"}));
if (isOn(4) && !shield) protections.push(t('gants', {ns: "common"}));
if (isOn(4) && shield) protections.push(t('gantMainsArmées', {ns: "common"}));
if (isOn(5) && shield) protections.push(t('gantMainBouclier', {ns: "common"}));
if (isOn(6)) protections.push(t('plastron', {ns: "common"}));
if (isOn(7) && !shield) protections.push(t('protectionDeBras', {ns: "common"}));
if (isOn(7) && shield) protections.push(t('protectionDeBrasArmé', {ns: "common"}));
if (isOn(8) && shield) protections.push(t('protectionDeBrasDeBouclier', {ns: "common"}));
if (isOn(9)) protections.push(t('protectionDeJambes', {ns: "common"}));
if (isOn(10)) protections.push(t('protectionDeGenoux', {ns: "common"}));
if (isOn(11)) protections.push(t('protectionDeCoudes', {ns: "common"}));
if (isOn(12)) protections.push(t('protectionDorsale', {ns: "common"}));
if (isOn(13)) protections.push(t('protectionDePieds', {ns: "common"}));
return protections;
}

View File

@ -1,8 +1,14 @@
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import React, {useEffect, useId, useState} from "react";
import {Trans, useTranslation} from "react-i18next";
import {useCountries} from "../../hooks/useCountries.jsx";
import {ListPresetSelect} from "./ListPresetSelect.jsx";
import {CatList, getCatName} from "../../utils/Tools.js";
import {useCombs} from "../../hooks/useComb.jsx";
import {toast} from "react-toastify";
import {build_tree} from "../../utils/TreeUtils.js";
import {createMatch} from "../../utils/CompetitionTools.js";
import {useRequestWS, useWS} from "../../hooks/useWS.jsx";
import {AxiosError} from "../AxiosError.jsx";
export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) {
const country = useCountries('fr')
@ -52,64 +58,6 @@ export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1
if (data != null)
applyFilter(data, dispoFiltered);
const makePoule = (combIn, groups) => {
combIn = combIn.sort(() => Math.random() - 0.5);
const maxInPoule = Math.ceil(combIn.length / 2);
const out = []
const pa = [];
const pb = [];
let nameA;
let nameB;
groups.forEach(g => {
const existsInCombIn = combIn.some(c => c.id === g.id);
if (existsInCombIn) {
if ((pa.length === 0 || g.poule === nameA) && pa.length < maxInPoule) {
nameA = g.poule || "1";
pa.push(g.id);
} else if ((pb.length === 0 || g.poule === nameB) && pb.length < maxInPoule) {
if (!(nameA === (g.poule || (nameA === "1" ? "2" : "1")))) {
nameB = g.poule || (nameA === "1" ? "2" : "1");
pb.push(g.id);
}
}
}
});
nameA = nameA || (nameB === "1" ? "2" : "1");
nameB = nameB || (nameA === "1" ? "2" : "1");
if (combIn.length <= 5) {
combIn.forEach(c => {
if (!pa.includes(c.id))
pa.push(c.id)
});
} else {
for (const c of combIn) {
if (pa.includes(c.id) || pb.includes(c.id))
continue;
const club = c.club_str || (c.teamMembers && c.teamMembers[0].club_str) || "";
const countInPa = pa.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
const countInPb = pb.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
if (pa.length < maxInPoule && (countInPa <= countInPb || pb.length >= maxInPoule)) {
pa.push(c.id);
} else if (pb.length < maxInPoule) {
pb.push(c.id);
} else {
pa.push(c.id);
}
}
}
pa.forEach(id => out.push({id: id, poule: nameA}));
pb.forEach(id => out.push({id: id, poule: nameB}));
return out
}
const handleSubmit = (e) => {
e.preventDefault();
@ -230,3 +178,625 @@ export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1
</div>
</>
}
function makePoule(combIn, groups) {
combIn = combIn.sort(() => Math.random() - 0.5);
const maxInPoule = Math.ceil(combIn.length / 2);
const out = []
const pa = [];
const pb = [];
let nameA;
let nameB;
groups.forEach(g => {
const existsInCombIn = combIn.some(c => c.id === g.id);
if (existsInCombIn) {
if ((pa.length === 0 || g.poule === nameA) && pa.length < maxInPoule) {
nameA = g.poule || "1";
pa.push(g.id);
} else if ((pb.length === 0 || g.poule === nameB) && pb.length < maxInPoule) {
if (!(nameA === (g.poule || (nameA === "1" ? "2" : "1")))) {
nameB = g.poule || (nameA === "1" ? "2" : "1");
pb.push(g.id);
}
}
}
});
nameA = nameA || (nameB === "1" ? "2" : "1");
nameB = nameB || (nameA === "1" ? "2" : "1");
if (combIn.length <= 5) {
combIn.forEach(c => {
if (!pa.includes(c.id))
pa.push(c.id)
});
} else {
for (const c of combIn) {
if (pa.includes(c.id) || pb.includes(c.id))
continue;
const club = c.club_str || (c.teamMembers && c.teamMembers[0].club_str) || "";
const countInPa = pa.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
const countInPb = pb.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
if (pa.length < maxInPoule && (countInPa <= countInPb || pb.length >= maxInPoule)) {
pa.push(c.id);
} else if (pb.length < maxInPoule) {
pb.push(c.id);
} else {
pa.push(c.id);
}
}
}
pa.forEach(id => out.push({id: id, poule: nameA}));
pb.forEach(id => out.push({id: id, poule: nameB}));
return out;
}
function makeWeightCategories(combs) {
combs = combs.filter(c => c.weight != null).sort((a, b) => a.weight - b.weight); // Add random for same weight ?
const catCount = Math.ceil(combs.length / 10);
const catSize = combs.length / catCount;
const catMaxSize = Math.min(Math.ceil(catSize), 10);
const catMinSize = Math.max(Math.floor(catSize), 3); // Add marge ?
const categories = Array.from({length: catCount}, () => []);
for (let i = 0; i < combs.length; i++) {
categories[Math.floor(i / catSize)].push(combs[i]);
}
let change = false;
let maxIterations = 500;
do {
change = false;
// ------ move in upper direction if better and possible ------
let needFree = -1;
let dIfFree = 0;
for (let i = 0; i < catCount - 1; i++) {
const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight;
const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight;
if (weightDiff > nextWeightDiff && categories.at(i).length > catMinSize) {
if (categories.at(i + 1).length < catMaxSize) {
const movedComb = categories.at(i).pop();
categories.at(i + 1).unshift(movedComb);
change = true;
} else if (weightDiff - nextWeightDiff > dIfFree) {
needFree = i;
dIfFree = weightDiff - nextWeightDiff;
}
}
}
if (needFree !== -1) {
let haveSpace = -1;
let maxDiff = 0;
for (let i = needFree + 1; i < catCount; i++) {
if (categories.at(i).length < catMaxSize) {
haveSpace = i;
break;
}
}
if (haveSpace !== -1) {
for (let i = needFree + 1; i < haveSpace; i++) {
const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight;
const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight;
const diffIfFree = weightDiff - nextWeightDiff;
if (diffIfFree > maxDiff) {
maxDiff = diffIfFree;
}
}
if (maxDiff < dIfFree) {
for (let i = needFree; i < haveSpace; i++) {
const movedComb = categories.at(i).pop();
categories.at(i + 1).unshift(movedComb);
change = true;
}
}
}
}
// ------ move in lower direction if better and possible ------
needFree = -1;
dIfFree = 0;
for (let i = 1; i < catCount; i++) {
const currentFirst = categories[i][0];
const currentSecondFirst = categories[i][1];
const prevLast = categories[i - 1][categories[i - 1].length - 1];
const weightDiff = currentSecondFirst.weight - currentFirst.weight;
const prevWeightDiff = currentFirst.weight - prevLast.weight;
if (weightDiff > prevWeightDiff && categories.at(i).length > catMinSize) {
if (categories.at(i - 1).length < catMaxSize) {
const movedComb = categories.at(i).shift();
categories.at(i - 1).push(movedComb);
change = true;
} else if (weightDiff - prevWeightDiff > dIfFree) {
needFree = i;
dIfFree = weightDiff - prevWeightDiff;
}
}
}
if (needFree !== -1) {
let haveSpace = -1;
let maxDiff = 0;
for (let i = needFree - 1; i >= 0; i--) {
if (categories.at(i).length < catMaxSize) {
haveSpace = i;
break;
}
}
if (haveSpace !== -1) {
for (let i = needFree - 1; i > haveSpace; i--) {
const currentFirst = categories[i][0];
const currentSecondFirst = categories[i][1];
const prevLast = categories[i - 1][categories[i - 1].length - 1];
const weightDiff = currentSecondFirst.weight - currentFirst.weight;
const prevWeightDiff = currentFirst.weight - prevLast.weight;
const diffIfFree = weightDiff - prevWeightDiff;
if (diffIfFree > maxDiff) {
maxDiff = diffIfFree;
}
}
if (maxDiff < dIfFree) {
for (let i = needFree; i > haveSpace; i--) {
const movedComb = categories.at(i).shift();
categories.at(i - 1).push(movedComb);
change = true;
}
}
}
}
} while (change && maxIterations-- > 0);
return categories;
}
const getCatNameList = (count) => {
const catNameList = [];
if (count >= 10) catNameList.push("Paille");
if (count >= 9) catNameList.push("Mouche");
if (count >= 8) catNameList.push("Coq");
if (count >= 7) catNameList.push("Plume");
if (count >= 2) catNameList.push("Léger");
if (count >= 5) catNameList.push("Mi-moyen");
if (count >= 3) catNameList.push("Moyen");
if (count >= 6) catNameList.push("Mi-lourd");
if (count >= 1) catNameList.push("Lourd");
if (count >= 4) catNameList.push("Super-lourd");
return catNameList;
}
function makeCategory(combs) {
const out = Array.from(CatList, (v, i) => ({
h: combs.filter(c => c.categorie === v && c.genre !== "F"),
f: combs.filter(c => c.categorie === v && c.genre === "F"),
m: [],
canMakeGenreFusion: i <= CatList.indexOf("BENJAMIN"),
c: v,
c_index: i,
min_c_index: i,
done: false
}))
for (let i = 0; i < out.length; i++)
out[i].done = out[i].h.length === 0 && out[i].f.length === 0;
for (let i = 0; i < out.length - 1; i++) {
const p = i === 0 ? undefined : out[i - 1];
const c = out[i];
const n = out[i + 1];
if (c.done)
continue;
if (c.canMakeGenreFusion) {
if (c.f.length < 6 || c.h.length < 5) {
if (c.f.length + c.h.length >= 3) {
c.m = c.h.concat(c.f);
c.h = [];
c.f = [];
c.done = true;
} else {
n.h = n.h.concat(c.h);
n.f = n.f.concat(c.f);
n.min_c_index = c.min_c_index
c.h = [];
c.f = [];
c.done = true;
}
} else {
c.done = true;
}
} else {
if (c.h.length < 3 && c.h.length > 0) {
if (p) {
if (p.h.length > 0 && p.h.length + c.h.length <= c.h.length + n.h.length && p.min_c_index - p.c_index < 1) {
p.h = p.h.concat(c.h);
c.h = [];
} else {
n.h = n.h.concat(c.h);
c.h = [];
}
} else {
n.h = n.h.concat(c.h);
c.h = [];
}
}
if (c.f.length < 3 && c.f.length > 0) {
if (p) {
if (p.f.length > 0 && p.f.length + c.f.length <= c.f.length + n.f.length && p.min_c_index - p.c_index < 1) {
p.f = p.f.concat(c.f);
c.f = [];
} else {
n.f = n.f.concat(c.f);
c.f = [];
}
} else {
n.f = n.f.concat(c.f);
c.f = [];
}
}
c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0);
}
}
// Down fusion if not done
for (let i = out.length - 1; i > 0; i--) {
const p = out[i - 1];
const c = out[i];
if (c.done)
continue;
if (c.h.length > 0 && c.h.length < 3) {
p.h = p.h.concat(c.h);
c.h = [];
}
if (c.f.length > 0 && c.f.length < 3) {
p.f = p.f.concat(c.f);
c.f = [];
}
c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0);
p.done = (p.h.length >= 3 || p.h.length === 0) && (p.f.length >= 3 || p.f.length === 0);
}
return out.map(c => [c.h, c.f, c.m]).flat().filter(l => l.length > 0);
}
function sendCatList(toastId, t, catList, sendRequest) {
toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0});
new Promise(async (resolve) => {
for (let i = 0; i < catList.length; i++) {
const progress = (i + 1) / catList.length;
toast.update(toastId.current, {progress});
const g = []
if (catList[i].combs.some(c => c.genre === "H")) g.push('H');
if (catList[i].combs.some(c => c.genre === "F")) g.push('F');
const cat = []
catList[i].combs.forEach(c => {
if (!cat.includes(c.categorie))
cat.push(c.categorie);
})
const type = catList[i].combs.length > 5 && catList[i].classement ? 3 : 1;
const newCat = {
name: catList[i].preset.name + " - " + cat.map(c => getCatName(c)).join(", ") +
(g.length === 2 ? "" : " - " + g.join("/")) + (catList[i].size === 1 ? "" : " - " + getCatNameList(catList[i].size)[catList[i].index]),
liceName: catList[i].lice,
type: type,
treeAreClassement: catList[i].classement,
fullClassement: catList[i].fullClassement,
preset: {id: catList[i].preset.id}
}
console.log(newCat)
await sendRequest('createOrReplaceCategory', newCat).then(id => {
newCat["id"] = id;
const groups = makePoule(catList[i].combs, []);
const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups);
const p = [];
p.push(sendRequest("recalculateMatch", {
categorie: newCat.id,
newMatch,
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: []
}).then(() => {
console.log("Finished creating matches for category", newCat.name);
}).catch(err => {
console.error("Error creating matches for category", newCat.name, err);
}))
if (type === 3) {
const trees = build_tree(4, 1)
console.log("Creating trees for new category:", trees);
p.push(sendRequest('updateTrees', {
categoryId: id,
trees: trees
}).then(() => {
console.log("Finished creating trees for category", newCat.name);
}).catch(err => {
console.error("Error creating trees for category", newCat.name, err);
}))
}
return Promise.allSettled(p)
}).catch(err => {
console.error("Error creating category", newCat.name, err);
})
console.log("Finished category", i + 1, "/", catList.length);
}
resolve();
}).finally(() => {
toast.done(toastId.current);
})
}
export function AutoNewCatModalContent() {
const {t} = useTranslation("cm");
const {combs} = useCombs();
const {sendRequest} = useWS();
const toastId = React.useRef(null);
const [gender, setGender] = useState({H: false, F: false, NA: false})
const [cat, setCat] = useState([])
const [preset, setPreset] = useState(undefined)
const [lice, setLice] = useState("1")
const [classement, setClassement] = useState(true)
const [fullClassement, setFullClassement] = useState(false)
const setCat_ = (e, index) => {
if (e.target.checked) {
if (!cat.includes(index)) {
setCat([...cat, index])
}
} else {
setCat(cat.filter(c => c !== index))
}
}
function applyFilter(dataIn, dataOut) {
dataIn.forEach(comb => {
if (comb == null)
return;
if ((gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
&& (cat.includes(Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)))
&& (preset === undefined || comb.categoriesInscrites?.includes(preset.id))) {
dataOut.push(comb)
}
}
)
}
const dispoFiltered = [];
if (combs != null)
applyFilter(Object.values(combs), dispoFiltered);
const handleSubmit = (e) => {
e.preventDefault();
let catList
if (dispoFiltered.length > 10) {
catList = makeWeightCategories(dispoFiltered);
} else {
catList = [[...dispoFiltered]];
}
console.log(catList.map(c => c.map(c => ({id: c.id, weight: c.weight, fname: c.fname, lname: c.lname}))))
sendCatList(toastId, t, catList
.map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})), sendRequest);
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="autoNewCatModalLabel">{t('depuisUneCatégoriePrédéfinie')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="d-flex flex-wrap justify-content-around">
<ListPresetSelect value={preset} onChange={setPreset} returnId={false}/>
<div>
<label className="form-label">{t('genre')}</label>
<div className="d-flex align-items-center">
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
onChange={e => setGender((prev) => {
return {...prev, H: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
</div>
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
onChange={e => setGender((prev) => {
return {...prev, F: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
onChange={e => setGender((prev) => {
return {...prev, NA: e.target.checked}
})}/>
<label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
</div>
</div>
</div>
</div>
{preset !== undefined && <>
<div className="d-flex flex-wrap justify-content-around mb-1">
<div className="d-flex flex-wrap mb-3">
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
{t('catégorie')} :
</label>
{preset.categories.map(c => [c.categorie, CatList.indexOf(c.categorie)]).sort((a, b) => a[1] - b[1])
.map(([cat_, index]) => {
return <div key={index} className="input-group"
style={{display: "contents"}}>
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox"
id={"categoriesInput" + index} checked={cat.includes(index)} aria-label={getCatName(cat_)}
onChange={e => setCat_(e, index)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{getCatName(cat_)}</label>
</div>
</div>
})}
</div>
</div>
</>}
<div className="mb-3">
<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="liceInput2" placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault" checked={classement}
onChange={e => setClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault">
{t('créerLaPhaseFinaleSilYADesPoules')}
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!classement}
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault2">
{t('lesCombattantsEnDehors2')}
</label>
</div>
<span>{dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')}</span><br/>
<span>{Math.ceil(dispoFiltered.length / 10)} {t('catégoriesVontêtreCréées')}</span><br/>
{dispoFiltered.length > 10 && dispoFiltered.some(c => !c.weight) &&
<span style={{color: "red"}}>{t('certainsCombattantsNontPasDePoidsRenseigné')}</span>}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" onClick={handleSubmit}
disabled={dispoFiltered.length <= 0} data-bs-dismiss="modal">{t('ajouter')}</button>
</div>
</>
}
export function AutoNewCatSModalContent() {
const {t} = useTranslation("cm");
const {combs} = useCombs();
const {sendRequest} = useWS();
const toastId = React.useRef(null);
const {data, error} = useRequestWS("listPreset", {}, null);
const id = useId()
const [categories, setCategories] = useState([])
const [lice, setLice] = useState("1")
const [classement, setClassement] = useState(true)
const [fullClassement, setFullClassement] = useState(false)
const setCategories_ = (e, catId) => {
if (e.target.checked) {
if (!categories.includes(catId)) {
setCategories([...categories, catId])
}
} else {
setCategories(categories.filter(c => c !== catId))
}
}
const handleSubmit = (e) => {
e.preventDefault();
let catList2 = []
for (const catId of categories) {
const preset = data.find(p => p.id === catId);
const dispoFiltered = Object.values(combs).filter(comb => comb.categoriesInscrites?.includes(catId)).sort(() => Math.random() - 0.5)
.map(comb => ({...comb, categorie: CatList[Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)]}));
console.log("Creating category for preset", preset.name, "and", dispoFiltered.length, "combattants");
const catList = makeCategory(dispoFiltered);
console.log(catList)
for (const list of catList) {
if (list.length > 10) {
catList2.push(...makeWeightCategories(list)
.map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})));
} else {
catList2.push(({combs: [...list], classement, preset, lice, fullClassement, index: 1, size: 1}));
}
}
}
sendCatList(toastId, t, catList2, sendRequest);
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="autoNewCatSModalLabel">{t('créerToutesLesCatégories')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="d-flex flex-wrap justify-content-around">
<div className="d-flex flex-wrap mb-3">
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
{t('catégorie')} :
</label>
{error ? <AxiosError error={error}/> : <>
{data && data.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
{data && data.map((cat, index) =>
<div key={cat.id} className="input-group"
style={{display: "contents"}}>
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox"
id={id + "categoriesInput" + index} checked={categories.includes(cat.id)} aria-label={cat.name}
onChange={e => setCategories_(e, cat.id)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={id + "categoriesInput" + index}>{cat.name}</label>
</div>
</div>)}
</>}
</div>
</div>
<div className="mb-3">
<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="liceInput3" placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault" checked={classement}
onChange={e => setClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault">
{t('créerLaPhaseFinaleSilYADesPoules')}
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!classement}
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkDefault2">
{t('lesCombattantsEnDehors2')}
</label>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" onClick={handleSubmit}>{t('ajouter')}</button>
</div>
</>
}

View File

@ -3,7 +3,7 @@ import {AxiosError} from "../AxiosError.jsx";
import {useTranslation} from "react-i18next";
import React, {useId} from "react";
export function ListPresetSelect({disabled, value, onChange}) {
export function ListPresetSelect({disabled, value, onChange, returnId = true}) {
const id = useId()
const {data, error} = useRequestWS("listPreset", {}, null);
const {t} = useTranslation();
@ -12,9 +12,16 @@ export function ListPresetSelect({disabled, value, onChange}) {
? <div className="mb-3">
<label className="form-label" htmlFor={id}>{t('catégorie')}</label>
<select className="form-select" id={id} disabled={disabled}
value={value} onChange={e => onChange(Number(e.target.value))}>
value={returnId ? value : (value ? value.id : -1)}
onChange={e => {
if (returnId) {
onChange(Number(e.target.value))
} else {
onChange(data.find(c => c.id === Number(e.target.value)))
}
}}>
<option value={-1}>{t('sélectionner...')}</option>
{data.sort((a, b) => a.name.localeCompare(b.name)).map(club => (<option key={club.id} value={club.id}>{club.name}</option>))}
{data.sort((a, b) => a.name.localeCompare(b.name)).map(o => (<option key={o.id} value={o.id}>{o.name}</option>))}
</select>
</div>
: error

View File

@ -554,11 +554,11 @@ function Modal_({data2, data3, error2, sendRegister, modalState, setModalState,
<div className="input-group mb-3">
<span className="input-group-text" id="weight">{t('comp.modal.poids')}</span>
{source === "admin" && <span className="input-group-text" id="weightReal">{t('comp.modal.annoncé')}</span>}
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weight"
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weight"
name="weight" aria-describedby="weight" disabled={!(data3.requiredWeight.includes(currenCat))} value={weight}
onChange={e => setWeight(e.target.value)}/>
{source === "admin" && <><span className="input-group-text" id="weightReal">{t('comp.modal.pesé')}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weightReal"
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weightReal"
name="weightReal" aria-describedby="weightReal" value={weightReal}
onChange={e => setWeightReal(e.target.value)}/></>}
</div>

View File

@ -156,11 +156,11 @@ function SelfRegister({data2}) {
<h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4>
<div className="input-group mb-3" hidden={!(data2?.requiredWeight.includes(currenCat))}>
<span className="input-group-text" id="weight">{t("comp.modal.poids")}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weight" disabled={disabled}
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weight" disabled={disabled}
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
{data[0]?.weightReal && <>
<span className="input-group-text" id="weight">{t("comp.modal.pesé")}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weight" disabled={true}
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weight" disabled={true}
name="weight" aria-describedby="weight" value={data[0]?.weightReal} onChange={() => {
}}/>
</>}

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 {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
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 {CategoryContent} from "./CategoryAdminContent.jsx";
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
@ -11,15 +11,17 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
import {faFile, faGlobe, 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 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 {StateWindow} from "./StateWindow.jsx";
import {CombName, useCombs} from "../../../hooks/useComb.jsx";
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx";
import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
import {makePDF} from "../../../utils/cmPdf.js";
const vite_url = import.meta.env.VITE_URL;
@ -50,7 +52,7 @@ export function CMAdmin({compUuid}) {
return <>
<div className="card">
<div className='card-header'>
<CategoryHeader cat={cat} setCatId={setCatId}/>
<CategoryHeader cat={cat} setCatId={setCatId} menuActions={menuActions}/>
</div>
<div className="card-body">
@ -176,6 +178,7 @@ function Menu({menuActions, compUuid}) {
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
const teamCardModal = useRef(null);
const printModal = useRef(null);
const {t} = useTranslation("cm");
const [showStateWin, setShowStateWin] = useState(false)
@ -264,7 +267,8 @@ function Menu({menuActions, compUuid}) {
}
const copyScriptToClipboard = () => {
navigator.clipboard.writeText(`<!--suppress ALL -->
// noinspection JSFileReferences
navigator.clipboard.writeText(`
<div id='safca_api_data'></div>
<script type="module">
import {initCompetitionApi} from '${vite_url}/competition.js';
@ -289,6 +293,11 @@ function Menu({menuActions, compUuid}) {
onMouseUp={() => longPressUp("cards")}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t("carton")}/>
<FontAwesomeIcon icon={faPrint} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onClick={() => printModal.current.click()}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('imprimer')}/>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("obs")}
@ -374,6 +383,151 @@ function Menu({menuActions, compUuid}) {
</div>
</div>
</div>
<button ref={printModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#PrintModal"
style={{display: 'none'}}>
Launch printModal
</button>
<div className="modal fade" id="PrintModal" tabIndex="-1" aria-labelledby="PrintModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<PrintModal menuActions={menuActions}/>
</div>
</div>
</div>
</>
}
function PrintModal({menuActions}) {
const [categorie, setCategorie] = 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 [presetSelect, setPresetSelect] = useState(-1)
const {sendRequest, welcomeData} = useWS();
const {getComb} = useCombs();
const {t} = useTranslation("cm");
const podiumPromise = (podiumRank_) => {
return sendRequest("getPodium", {}).then(data => {
return [welcomeData?.name + " - " + "Podium", [
{type: "podium", params: ({data, maxRank: podiumRank_, minRank: Math.min(4, podiumRank_)})},
]];
});
}
const print = (action) => {
const pagesPromise = [];
if (categorie && menuActions.printCategorie)
pagesPromise.push(menuActions.printCategorie(categorieEmpty))
if (preset && menuActions.printCategoriePreset)
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));
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 <>
<div className="modal-header">
<h5 className="modal-title">{t('quoiImprimer?')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint"
onChange={e => setCategorie(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint">{t('catégorieSélectionnée')}</label>
</div>
{categorie &&
<div className="form-check" style={{marginLeft: "1em"}}>
<input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2"
onChange={e => setCategorieEmpty(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint2">{t('feuilleVierge')}</label>
</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">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>
<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("download")}>{t('enregistrer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("print")}>{t('imprimer')}</button>
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
</div>
</>
}
@ -500,12 +654,11 @@ function TeamCardModal() {
</>
}
function CategoryHeader({
cat, setCatId
}) {
function CategoryHeader({cat, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const bthRef = useRef();
const confirmRef = useRef();
const bthRef = useRef(null);
const newBthRef = useRef(null);
const confirmRef = useRef(null);
const [modal, setModal] = useState({})
const [confirm, setConfirm] = useState({})
const {t} = useTranslation("cm");
@ -521,6 +674,7 @@ function CategoryHeader({
])
}
const sendAddCategory = ({data}) => {
console.log("add cat", data);
setCats([...cats, data])
}
const sendDelCategory = ({data}) => {
@ -554,8 +708,7 @@ function CategoryHeader({
if (selectedCatId !== "-1") {
setCatId(selectedCatId);
} else { // New category
setModal({});
bthRef.current.click();
newBthRef.current.click();
e.target.value = cat?.id;
}
}
@ -595,14 +748,155 @@ function CategoryHeader({
</div>
</div>
<div className="modal fade" id="autoNewCatModal" aria-hidden="true" aria-labelledby="autoNewCatModalLabel" tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<AutoNewCatModalContent/>
</div>
</div>
</div>
<div className="modal fade" id="autoNewCatsModal" aria-hidden="true" aria-labelledby="autoNewCatsModalLabel" tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<AutoNewCatSModalContent/>
</div>
</div>
</div>
<button ref={confirmRef} data-bs-toggle="modal" data-bs-target="#confirm-dialog" style={{display: "none"}}>open</button>
<ConfirmDialog id="confirm-dialog" onConfirm={confirm.confirm ? confirm.confirm : () => {
}} onCancel={confirm.cancel ? confirm.cancel : () => {
}} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/>
<button ref={newBthRef} data-bs-toggle="modal" data-bs-target="#newCatModal" style={{display: "none"}}>open</button>
<div className="modal fade" id="newCatModal" tabIndex="-1" aria-labelledby="newCatModalLabel" aria-hidden="true">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h1 className="modal-title fs-5" id="newCatModalLabel">{t('ajouter')} {t('uneCatégorie')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body text-center">
{t('modeDeCréation')} :
<div className="mb-2">
<button className="btn btn-primary" onClick={() => {
setModal({});
bthRef.current.click();
e.target.value = cat?.id;
}}>{t('personnaliser')}</button>
</div>
<div className="mb-2">
<button className="btn btn-primary" data-bs-target="#autoNewCatModal"
data-bs-toggle="modal">{t('depuisUneCatégoriePrédéfinie')}</button>
</div>
<div className="mb-2">
<button className="btn btn-primary" data-bs-target="#autoNewCatsModal"
data-bs-toggle="modal">{t('créerToutesLesCatégories')}</button>
</div>
</div>
</div>
</div>
</div>
<PrintCats menuActions={menuActions} cats={cats}/>
</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}) {
const id = useId()
const [name, setName] = useState("")
const [lice, setLice] = useState("1")
const [poule, setPoule] = useState(true)
@ -617,6 +911,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const {sendRequest} = useWS();
useEffect(() => {
console.log(state);
setName(state.name || "");
setLice(state.liceName || "1");
setPoule(((state.type || 1) & 1) !== 0);
@ -784,8 +1079,9 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
<ListPresetSelect value={preset} onChange={setPreset}/>
<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>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice}
<label htmlFor={id + "liceInput1"} 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={id + "liceInput1"} placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/>
</div>

View File

@ -31,6 +31,12 @@ function CupImg() {
alt=""/>
}
function CupImg2() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png"
alt=""/>
}
export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
@ -311,9 +317,9 @@ function MatchList({matches, cat, menuActions, classement = false, currentMatch
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = classement
? matches.filter(m => m.categorie_ord === -42)
? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
: matches.filter(m => m.categorie_ord !== -42)
: matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
@ -443,14 +449,14 @@ function MatchList({matches, cat, menuActions, classement = false, currentMatch
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>}
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{index >= firstIndex ? index + 1 - firstIndex : ""}</th>
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && ((m.win > 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
<small className="position-relative"><CombName combId={m.c1}/>
<GetCard match={m} combId={m.c1} cat={cat}/></small></td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
<small className="position-relative"><CombName combId={m.c2}/>
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && ((m.win < 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
</tr>
))}
</tbody>

View File

@ -31,6 +31,12 @@ function CupImg() {
alt=""/>
}
function CupImg2() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png"
alt=""/>
}
export function CategoryContent({cat, catId, setCat, menuActions}) {
const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS();
@ -80,6 +86,9 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
if (data.categorie !== cat.id)
continue;
setGroups(prev => {
if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id))
return [...prev, {id: data.c1?.id, poule: data.poule}];
@ -175,9 +184,28 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
<div className="col-md-9">
{cat && <ListMatch cat={cat} matches={matches} groups={groups} reducer={reducer}/>}
</div>
<PrintMatch menuActions={menuActions} matches={matches} groups={groups} cat={cat}/>
</>
}
function PrintMatch({menuActions, cat, groups, matches}) {
const {cards_v} = useCards();
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
marches2.forEach(m => {
if (m.end && (!m.scores || m.scores.length === 0))
m.scores = [{n_round: 0, s1: 0, s2: 0}];
})
menuActions.printCategorie = (categorieEmpty) => {
return [cat.name, [
{type: "categorie", params: ({cat, matches: marches2, groups, cards_v, categorieEmpty})}
]]
}
return <></>
}
function AddComb({groups, setGroups, removeGroup, menuActions, cat}) {
const {data, setData} = useRequestWS("getRegister", null)
const combDispatch = useCombsDispatch()
@ -428,7 +456,7 @@ function ListMatch({cat, matches, groups, reducer}) {
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: matchesToRemove.map(m => m.id)
}), getToastMessage("toast.matchs.create", "ns"))
}), getToastMessage("toast.matchs.create", "cm"))
.finally(() => {
console.log("Finished creating matches");
})
@ -516,9 +544,9 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = classement
? matches.filter(m => m.categorie_ord === -42)
? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
: matches.filter(m => m.categorie_ord !== -42)
: matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
@ -712,7 +740,8 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
<th style={{textAlign: "center", cursor: "auto"}} scope="row">{index + 1}</th>
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>}
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</td>}
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && ((m.win > 0 &&
<CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}
onClick={e => handleCombClick(e, m.id, m.c1)}>
<small className="position-relative"><CombName combId={m.c1}/>
@ -721,7 +750,8 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
onClick={e => handleCombClick(e, m.id, m.c2)}>
<small className="position-relative"><CombName combId={m.c2}/>
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && ((m.win < 0 &&
<CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "center", cursor: "auto"}}>{scoreToString2(m, cards_v)}</td>
<td style={{textAlign: "center", cursor: "pointer", color: "#1381ff"}} onClick={_ => handleEditMatch(m.id)}>
<FontAwesomeIcon icon={faPen}/></td>

View File

@ -156,7 +156,7 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)
&& (teamMode && (comb.teamMembers == null || comb.teamMembers.length === 0) || !teamMode
&& ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team))
&& (preset === -1 || comb.categoriesInscrites.includes(preset))) {
&& (preset === -1 || comb.categoriesInscrites?.includes(preset))) {
dataOut[id] = dataIn[id];
}
}

View File

@ -1,6 +1,6 @@
import {useEffect, useRef} from "react";
import {scorePrint} from "../../utils/Tools.js";
import {compareCardOrder, useCardsStatic} from "../../hooks/useCard.jsx";
import {useCardsStatic} from "../../hooks/useCard.jsx";
const max_x = 500;
@ -451,3 +451,279 @@ export function DrawGraph({
<canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px", position: "relative", opacity: 1}} id="myCanvas"></canvas>
</div>
}
export function drawGraphForPdf(root = [], size = 14, cards = []) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards);
const sizeY = size * 0.5;
function getBounds(root) {
let px = max_x;
let py;
let maxx, minx, miny, maxy
function drawNode(tree, px, py) {
let death = tree.death() - 1
if (death === 0) {
if (miny > py - sizeY - ((sizeY * 1.5 / 2) | 0)) miny = py - sizeY - (sizeY * 1.5 / 2) | 0;
if (maxy < py + sizeY + ((sizeY * 1.5 / 2) | 0)) maxy = py + sizeY + (sizeY * 1.5 / 2) | 0;
} else {
if (miny > py - sizeY * 2 * death - ((sizeY * 1.5 / 2) | 0))
miny = py - sizeY * 2 * death - ((sizeY * 1.5 / 2) | 0);
if (maxy < py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0))
maxy = py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0);
}
if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8;
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - sizeY * 2 * death);
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + sizeY * 2 * death);
}
if (root != null) {
py = (sizeY * 2 * root.at(0).death() + (((sizeY * 1.5 / 2) | 0) + sizeY) * root.at(0).death()) * 2;
maxx = px;
minx = px;
miny = py - (sizeY * 1.5 / 2) | 0;
maxy = py + (sizeY * 1.5 / 2) | 0;
for (const node of root) {
px = px - size * 2 - size * 8;
if (minx > px) minx = px;
drawNode(node, px, py);
//graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny);
py = maxy + ((sizeY * 2 * node.death() + ((sizeY * 1.5 / 2) | 0)));
px = maxx;
}
} else {
minx = 0;
maxx = 0;
miny = 0;
maxy = 0;
}
return [minx, maxx, miny, maxy];
}
// Fonction pour dessiner du texte avec gestion de la taille
const printText = (ctx, s, x, y, width, height, lineG, lineD) => {
ctx.save();
ctx.translate(x, y);
let tSize = 17;
let ratioX = height * 1.0 / 20.0;
ctx.font = "100 " + tSize + "px Arial";
let mw = width - (ratioX * 2) | 0;
if (ctx.measureText(s).width > mw) {
do {
tSize--;
ctx.font = tSize + "px Arial";
} while (ctx.measureText(s).width > mw && tSize > 10);
if (ctx.measureText(s).width > mw) {
let truncated = "";
const words = s.split(" ");
for (const word of words) {
if (ctx.measureText(truncated + word).width >= mw) {
truncated += "...";
break;
} else {
truncated += word + " ";
}
}
s = truncated;
}
}
const text = ctx.measureText(s);
let dx = (width - text.width) / 2;
let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillText(s, dx, dy, width - dy);
ctx.restore();
ctx.beginPath();
if (lineD) {
ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2);
ctx.lineTo(x + width, y + height / 2);
}
if (lineG) {
ctx.moveTo(x, y + height / 2);
ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2);
}
ctx.stroke();
};
// Fonction pour afficher les scores
const printScores = (ctx, scores, px, py, scale) => {
ctx.save();
ctx.translate(px - size * 2, py - size * scale);
ctx.font = "100 14px Arial";
ctx.textBaseline = 'top';
for (let i = 0; i < scores.length; i++) {
const score = scorePrint(scores[i].s1) + "-" + scorePrint(scores[i].s2);
const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3;
const text = ctx.measureText(score);
let dx = (size * 2 - text.width) / 2;
let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillStyle = '#ffffffdd';
ctx.fillRect(dx, size * 2 * scale / div * i + dy, text.width, 14);
ctx.fillStyle = "#000000";
ctx.fillText(score, dx, size * 2 * scale / div * i + dy, size * 2);
}
ctx.restore();
};
const printCard = (ctx, pos, combId, match) => {
const cards2 = getHeightCardForCombInMatch(combId, match)
if (cards2 != null) {
let oldColor = ctx.fillStyle;
switch (cards2.type) {
case "BLUE":
ctx.fillStyle = "#2e2efd";
break;
case "YELLOW":
ctx.fillStyle = "#d8d800";
break;
case "RED":
ctx.fillStyle = "#FF0000";
break;
case "BLACK":
ctx.fillStyle = "#000000";
break;
default:
ctx.fillStyle = "#FFFFFF00";
}
if (cards2.match === match.id) {
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle
ctx.arc(pos.x + pos.width - 10, pos.y + 5, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = "#000000"
} else
ctx.fillRect(pos.x + pos.width - 18, pos.y - 5, 12, 12);
ctx.fillStyle = oldColor;
}
}
// Fonction pour dessiner un nœud
const drawNode = (ctx, tree, px, py, max_y) => {
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px - size, py);
ctx.stroke();
let death = tree.death() - 1;
let match = tree.data;
if (death === 0) {
ctx.beginPath();
ctx.moveTo(px - size, py + sizeY);
ctx.lineTo(px - size, py - sizeY);
ctx.moveTo(px - size, py + sizeY);
ctx.lineTo(px - size * 2, py + sizeY);
ctx.moveTo(px - size, py - sizeY);
ctx.lineTo(px - size * 2, py - sizeY);
ctx.stroke();
printScores(ctx, match.scores, px, py, 1);
const pos = {x: px - size * 2 - size * 8, y: py - sizeY - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos, match.c1, match)
ctx.fillStyle = "#FF0000"
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true)
const pos2 = {x: px - size * 2 - size * 8, y: py + sizeY - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos2, match.c2, match)
ctx.fillStyle = "#0000FF"
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true)
if (max_y.current < py + sizeY + ((sizeY * 1.5 / 2) | 0)) {
max_y.current = py + sizeY + (sizeY * 1.5 / 2 | 0);
}
} else {
ctx.beginPath();
ctx.moveTo(px - size, py);
ctx.lineTo(px - size, py + sizeY * 2 * death);
ctx.moveTo(px - size, py);
ctx.lineTo(px - size, py - sizeY * 2 * death);
ctx.moveTo(px - size, py + sizeY * 2 * death);
ctx.lineTo(px - size * 2, py + sizeY * 2 * death);
ctx.moveTo(px - size, py - sizeY * 2 * death);
ctx.lineTo(px - size * 2, py - sizeY * 2 * death);
ctx.stroke();
printScores(ctx, match.scores, px, py, 1.5);
const pos = {x: px - size * 2 - size * 8, y: py - sizeY * 2 * death - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos, match.c1, match)
ctx.fillStyle = "#FF0000"
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true)
const pos2 = {x: px - size * 2 - size * 8, y: py + sizeY * 2 * death - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
printCard(ctx, pos2, match.c2, match)
ctx.fillStyle = "#0000FF"
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true)
if (max_y.current < py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0)) {
max_y.current = py + sizeY * 2 * death + ((sizeY * 1.5 / 2 | 0));
}
}
if (tree.left != null) {
drawNode(ctx, tree.left, px - size * 2 - size * 8, py - sizeY * 2 * death, max_y);
}
if (tree.right != null) {
drawNode(ctx, tree.right, px - size * 2 - size * 8, py + sizeY * 2 * death, max_y);
}
};
// Dessiner sur le canvas principal
const canvas = document.createElement('canvas');
canvas.id = "myCanvas";
canvas.style.border = "1px solid grey";
canvas.style.marginTop = "10px";
const ctx = canvas.getContext("2d");
const [minx, maxx, miny, maxy] = getBounds(root);
canvas.width = maxx - minx;
canvas.height = maxy - miny;
ctx.translate(-minx, -miny);
ctx.fillStyle = "#000000";
ctx.lineWidth = 2;
ctx.strokeStyle = "#000000";
let px = maxx;
let py;
const max_y = {current: 0};
py = (sizeY * 2 * root[0].death() + (((sizeY * 1.5 / 2) | 0) + sizeY) * root[0].death()) * 2;
max_y.current = py + (sizeY * 1.5 / 2 | 0);
for (const node of root) {
let win_name = "";
if (node.data.end) {
win_name = node.data.win > 0
? (node.data.c1FullName === null ? "???" : node.data.c1FullName)
: (node.data.c2FullName === null ? "???" : node.data.c2FullName);
}
ctx.fillStyle = "#18A918";
printText(ctx, win_name,
px - size * 2 - size * 8, py - ((sizeY * 1.5 / 2) | 0),
size * 8, (sizeY * 1.5 | 0), true, false);
px = px - size * 2 - size * 8;
drawNode(ctx, node, px, py, max_y);
py = max_y.current + ((sizeY * 2 * node.death() + ((sizeY * 1.5 / 2) | 0)));
px = maxx;
}
return canvas;
}

View File

@ -418,3 +418,29 @@ export function hex2rgb(hex) {
// 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,443 @@
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;
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, "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);
}