diff --git a/docker-compose.yml b/docker-compose.yml index 1de12bc..bd054c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,11 @@ services: - default - intra - nginx + healthcheck: + test: [ "CMD", "curl", "-f", "https://intra.ffsaf.fr/api" ] + interval: 30s + timeout: 5s + retries: 3 ffsaf-db: image: public.ecr.aws/docker/library/postgres:17.2 diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java new file mode 100644 index 0000000..56e3c65 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java @@ -0,0 +1,89 @@ +package fr.titionfire.ffsaf.data.model; + +import fr.titionfire.ffsaf.utils.Categorie; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Entity +@Table(name = "category_preset") +public class CatPresetModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "competition", referencedColumnName = "id") + CompetitionModel competition; + + String name = ""; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "category_preset_catconfig", joinColumns = @JoinColumn(name = "id_preset")) + List categories; + + SwordType swordType = SwordType.NONE; + ShieldType shieldType = ShieldType.NONE; + + /* + * 1 - 1 - Casque + * 2 - 2 - Gorgerin + * 3 - 4 - Coquille et Protection pelvienne + * 4 - 8 - Gant main(s) armée(s) + * 5 - 16 - Gant main bouclier + * 6 - 32 - Plastron + * 7 - 64 - Protection de bras armé(s) + * 8 - 128 - Protection de bras de bouclier + * 9 - 256 - Protection de jambes + * 10 - 512 - Protection de genoux + * 11 - 1024 - Protection de coudes + * 12 - 2048 - Protection dorsale + * 13 - 4096 - Protection de pieds + */ + int mandatoryProtection1 = 0; + int mandatoryProtection2 = 0; + + @ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY) + private List registers = new ArrayList<>(); + + @ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY) + private List guest = new ArrayList<>(); + + public enum SwordType { + NONE, + ONE_HAND, + TWO_HAND, + SABER + } + + public enum ShieldType { + NONE, + STANDARD, + ROUND, + TEARDROP, + BUCKLER + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @RegisterForReflection + + @Embeddable + public static class CategorieEmbeddable { + Categorie categorie; + long roundDuration; + long pauseDuration; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java index 5f381cf..de7720b 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CategoryModel.java @@ -44,4 +44,8 @@ public class CategoryModel { Integer type; String liceName = "1"; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "id_preset", referencedColumnName = "id") + CatPresetModel preset; } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java index 42f2a4a..fae3a91 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java @@ -44,6 +44,7 @@ public class CompetitionGuestModel implements CombModel { String country = "fr"; Integer weight = null; + Integer weightReal = null; @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinTable( @@ -61,6 +62,14 @@ public class CompetitionGuestModel implements CombModel { ) List guest = new ArrayList<>(); + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinTable( + name = "categories_insc_guest", + joinColumns = @JoinColumn(name = "guest_id"), + inverseJoinColumns = @JoinColumn(name = "category_id") + ) + List categoriesInscrites = new ArrayList<>(); + public CompetitionGuestModel(String s) { this.fname = s.substring(0, s.indexOf(" ")); this.lname = s.substring(s.indexOf(" ") + 1); @@ -99,4 +108,8 @@ public class CompetitionGuestModel implements CombModel { } return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_)); } + + public Integer getWeight2() { + return (this.weightReal != null) ? this.weightReal : this.weight; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java index c3e2d7c..5e136a9 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.data.model; +import fr.titionfire.ffsaf.utils.Categorie; import fr.titionfire.ffsaf.utils.CompetitionSystem; import fr.titionfire.ffsaf.utils.RegisterMode; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -58,6 +59,10 @@ public class CompetitionModel { @OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL) List guests = new ArrayList<>(); + @OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + List catPreset = new ArrayList<>(); + + List requiredWeight = new ArrayList<>(); List banMembre = new ArrayList<>(); diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 16691d3..75c03bd 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -11,6 +11,9 @@ import lombok.Setter; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; +import java.util.ArrayList; +import java.util.List; + @Getter @Setter @AllArgsConstructor @@ -35,6 +38,7 @@ public class RegisterModel { MembreModel membre; Integer weight; + Integer weightReal; int overCategory = 0; Categorie categorie; @@ -46,6 +50,17 @@ public class RegisterModel { @Column(nullable = false, columnDefinition = "boolean default false") boolean lockEdit = false; + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinTable( + name = "categories_insc_comb", + joinColumns = { + @JoinColumn(name = "id_competition", referencedColumnName = "id_competition"), + @JoinColumn(name = "id_membre", referencedColumnName = "id_membre") + }, + inverseJoinColumns = @JoinColumn(name = "category_id") + ) + List categoriesInscrites = new ArrayList<>(); + public RegisterModel(CompetitionModel competition, MembreModel membre, Integer weight, int overCategory, Categorie categorie, ClubModel club) { this.id = new RegisterId(competition.getId(), membre.getId()); @@ -75,4 +90,10 @@ public class RegisterModel { return null; return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)]; } + + public Integer getWeight2() { + if (weightReal != null) + return weightReal; + return weight; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/CatPresetRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/CatPresetRepository.java new file mode 100644 index 0000000..bd62519 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/CatPresetRepository.java @@ -0,0 +1,9 @@ +package fr.titionfire.ffsaf.data.repository; + +import fr.titionfire.ffsaf.data.model.CatPresetModel; +import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CatPresetRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java index 448b915..6babba7 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetPermService.java @@ -98,12 +98,14 @@ public class CompetPermService { map.putIfAbsent(model.getId(), "owner"); else if (securityCtx.roleHas("federation_admin")) map.putIfAbsent(model.getId(), "admin"); - else if (securityCtx.isInClubGroup(model.getClub().getId()) && (securityCtx.roleHas( - "club_president") - || securityCtx.roleHas("club_respo_intra") || securityCtx.roleHas( - "club_secretaire") - || securityCtx.roleHas("club_tresorier"))) + else if (securityCtx.isInClubGroup( + model.getClub().getId()) && (securityCtx.isClubAdmin())) map.putIfAbsent(model.getId(), "admin"); + else if (model.getAdmin().contains(securityCtx.getSubject())) + map.putIfAbsent(model.getId(), "admin"); + else if (model.getTable().contains(securityCtx.getSubject())) + map.putIfAbsent(model.getId(), "table"); + } return map; })); @@ -182,12 +184,14 @@ public class CompetPermService { if (o.getSystem() == CompetitionSystem.SAFCA) return hasSafcaViewPerm(securityCtx, o.getId()); + if (o.getAdmin().contains(securityCtx.getSubject())) + return Uni.createFrom().nullItem(); + if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here throw new DForbiddenException(); if (o.getSystem() == CompetitionSystem.INTERNAL) - if (securityCtx.roleHas("club_president") || securityCtx.roleHas("club_respo_intra") - || securityCtx.roleHas("club_secretaire") || securityCtx.roleHas("club_tresorier")) + if (securityCtx.isClubAdmin()) return Uni.createFrom().nullItem(); throw new DForbiddenException(); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index 03faff3..110482e 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -7,10 +7,7 @@ import fr.titionfire.ffsaf.net2.data.SimpleCompet; import fr.titionfire.ffsaf.net2.request.SReqCompet; import fr.titionfire.ffsaf.net2.request.SReqRegister; import fr.titionfire.ffsaf.rest.client.dto.NotificationData; -import fr.titionfire.ffsaf.rest.data.CompetitionData; -import fr.titionfire.ffsaf.rest.data.RegisterRequestData; -import fr.titionfire.ffsaf.rest.data.SimpleCompetData; -import fr.titionfire.ffsaf.rest.data.SimpleRegisterComb; +import fr.titionfire.ffsaf.rest.data.*; import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; @@ -63,6 +60,9 @@ public class CompetitionService { @Inject CompetitionGuestRepository competitionGuestRepository; + @Inject + CatPresetRepository catPresetRepository; + @Inject ServerCustom serverCustom; @@ -112,16 +112,14 @@ public class CompetitionService { public Uni getByIdAdmin(SecurityCtx securityCtx, Long id) { if (id == 0) { - return Uni.createFrom() - .item(new CompetitionData(null, "", "", "", "", new Date(), new Date(), - CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true, - null, "", "", null, true, true, - "", "", "", "", "{}")); + return Uni.createFrom().item(new CompetitionData()); } return permService.hasAdminViewPerm(securityCtx, id) + .call(competitionModel -> Mutiny.fetch(competitionModel.getCatPreset())) .chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc()) .chain(insc -> Mutiny.fetch(competitionModel.getGuests()) - .map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest)))) + .map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest) + .addPresets(competitionModel.getCatPreset())))) .chain(data -> vertx.getOrCreateContext().executeBlocking(() -> { keycloakService.getUser(UUID.fromString(data.getOwner())) @@ -208,17 +206,21 @@ public class CompetitionService { model.setGuests(new ArrayList<>()); model.setUuid(UUID.randomUUID().toString()); model.setOwner(securityCtx.getSubject()); + model.setCatPreset(new ArrayList<>()); copyData(data, model); return Panache.withTransaction(() -> repository.persist(model)); - }).map(CompetitionData::fromModel) + }) + .call(model -> syncPreset(data, model)) + .map(CompetitionData::fromModel) .call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()) .call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()); } else { return permService.hasEditPerm(securityCtx, data.getId()) + .call(model -> Mutiny.fetch(model.getCatPreset())) .chain(model -> { copyData(data, model); @@ -237,7 +239,9 @@ public class CompetitionService { } })) .chain(__ -> Panache.withTransaction(() -> repository.persist(model))); - }).map(CompetitionData::fromModel) + }) + .call(model -> syncPreset(data, model)) + .map(model -> CompetitionData.fromModel(model).addPresets(model.getCatPreset())) .call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate( securityCtx.getSubject()) : Uni.createFrom().nullItem()) .call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate( @@ -245,6 +249,45 @@ public class CompetitionService { } } + private Uni syncPreset(CompetitionData data, CompetitionModel model) { + List toRemoveId = model.getCatPreset().stream() + .map(CatPresetModel::getId) + .filter(id -> data.getPresets().stream().noneMatch(preset -> Objects.equals(preset.getId(), id))) + .toList(); + + for (PresetData preset : data.getPresets()) { + CatPresetModel presetModel; + if (preset.getId() != null && preset.getId() > 0) { + presetModel = model.getCatPreset().stream() + .filter(p -> p.getId().equals(preset.getId())) + .findFirst() + .orElse(new CatPresetModel()); + } else { + presetModel = new CatPresetModel(); + model.getCatPreset().add(presetModel); + } + + presetModel.setCompetition(model); + presetModel.setName(preset.getName()); + presetModel.setSwordType(preset.getSword()); + presetModel.setShieldType(preset.getShield()); + presetModel.setCategories(preset.getCategories()); + presetModel.setMandatoryProtection1(preset.getMandatoryProtection1()); + presetModel.setMandatoryProtection2(preset.getMandatoryProtection2()); + } + + // Remove deleted presets + model.getCatPreset().removeIf(presetModel -> toRemoveId.contains(presetModel.getId())); + + return Panache.withTransaction(() -> repository.persist(model) + .call(__ -> { + if (!toRemoveId.isEmpty()) { + return catPresetRepository.delete("id IN ?1", toRemoveId); + } + return Uni.createFrom().nullItem(); + })); + } + private void copyData(CompetitionData data, CompetitionModel model) { if (model.getBanMembre() == null) model.setBanMembre(new ArrayList<>()); @@ -258,6 +301,7 @@ public class CompetitionService { model.setStartRegister(data.getStartRegister()); model.setEndRegister(data.getEndRegister()); model.setRegisterMode(data.getRegisterMode()); + model.setRequiredWeight(data.getRequiredWeight()); model.setData1(data.getData1()); model.setData2(data.getData2()); model.setData3(data.getData3()); @@ -271,11 +315,18 @@ public class CompetitionService { Uni> uni = Mutiny.fetch(c.getInsc()) .onItem().transformToMulti(Multi.createFrom()::iterable) .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences())) - .map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences())) + .onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites())) + .map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences()) + .setCategorieInscrite(cm.getCategoriesInscrites())) .collect().asList(); return uni .call(l -> Mutiny.fetch(c.getGuests()) - .map(guest -> guest.stream().map(SimpleRegisterComb::fromModel).toList()) + .onItem().transformToMulti(Multi.createFrom()::iterable) + .filter(g -> !g.isTeam()) + .onItem().call(guest -> Mutiny.fetch(guest.getCategoriesInscrites())) + .map(guest -> SimpleRegisterComb.fromModel(guest) + .setCategorieInscrite(guest.getCategoriesInscrites())) + .collect().asList() .invoke(l::addAll)); }); @@ -290,12 +341,17 @@ public class CompetitionService { model.getClub())) .onItem().transformToMulti(Multi.createFrom()::iterable) .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences())) - .map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())) + .onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites())) + .map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences()) + .setCategorieInscrite(combModel.getCategoriesInscrites())) .collect().asList(); return membreService.getByAccountId(securityCtx.getSubject()) .chain(model -> registerRepository.find("competition.id = ?1 AND membre = ?2", id, model).firstResult() - .map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of())))); + .call(rm -> rm == null ? Uni.createFrom().voidItem() : + Mutiny.fetch(rm.getCategoriesInscrites())) + .map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of()) + .setCategorieInscrite(rm.getCategoriesInscrites())))); } public Uni addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data, @@ -311,8 +367,9 @@ public class CompetitionService { c.getBanMembre().remove(combModel.getId()); return Panache.withTransaction(() -> repository.persist(c)); }) - .chain(combModel -> updateRegister(data, c, combModel, true))) - .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())); + .chain(combModel -> updateRegister(data, c, combModel, true, false))) + .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) + .setCategorieInscrite(r.getCategoriesInscrites())); } else { return permService.hasEditPerm(securityCtx, id) .chain(c -> competitionGuestRepository.findById(data.getId() * -1) @@ -323,7 +380,7 @@ public class CompetitionService { model.setCompetition(c); return model; })) - .chain(model -> { + .invoke(model -> { model.setFname(data.getFname()); if (data.getLname().equals("__team")) model.setLname("_team"); @@ -332,15 +389,26 @@ public class CompetitionService { model.setGenre(data.getGenre()); model.setClub(data.getClub()); model.setCountry(data.getCountry()); - model.setWeight(data.getWeight()); + model.setWeightReal(data.getWeightReal()); model.setCategorie(data.getCategorie()); - - return Panache.withTransaction(() -> competitionGuestRepository.persist(model)) - .call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? - sRegister.sendRegister(model.getCompetition().getUuid(), - r) : Uni.createFrom().voidItem()); + if (model.getCompetition().getRequiredWeight().contains(model.getCategorie())) + model.setWeight(data.getWeight()); }) - .map(SimpleRegisterComb::fromModel); + .call(g -> Mutiny.fetch(g.getCategoriesInscrites())) + .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(), + data.getCategoriesInscrites()) + .invoke(cats -> { + g.getCategoriesInscrites().clear(); + g.getCategoriesInscrites().addAll(cats); + g.getCategoriesInscrites() + .removeIf(cat -> cat.getCategories().stream() + .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())) + .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites())); } if ("club".equals(source)) return repository.findById(id) @@ -359,8 +427,9 @@ public class CompetitionService { if (c.getBanMembre().contains(model.getId())) throw new DForbiddenException(trad.t("insc.err1")); })) - .chain(combModel -> updateRegister(data, c, combModel, false))) - .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())); + .chain(combModel -> updateRegister(data, c, combModel, false, false))) + .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) + .setCategorieInscrite(r.getCategoriesInscrites())); return repository.findById(id) .invoke(Unchecked.consumer(cm -> { @@ -374,20 +443,101 @@ public class CompetitionService { if (c.getBanMembre().contains(model.getId())) throw new DForbiddenException(trad.t("insc.err2")); })) - .chain(combModel -> updateRegister(data, c, combModel, false))) - .map(r -> SimpleRegisterComb.fromModel(r, List.of())); + .chain(combModel -> updateRegister(data, c, combModel, false, false))) + .map(r -> SimpleRegisterComb.fromModel(r, List.of()).setCategorieInscrite(r.getCategoriesInscrites())); + } + + public Uni> addRegistersComb(SecurityCtx securityCtx, Long id, + List datas, + String source) { + 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 -> { + SimpleRegisterComb errorComb = new SimpleRegisterComb(); + errorComb.setLicence(-42); + errorComb.setFname("ERROR"); + errorComb.setLname(t.getMessage()); + return errorComb; + })).concatenate().collect().asList(); + } + + private Uni makeImportUpdate(SecurityCtx securityCtx, Long id, 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()) + .call(combModel -> Mutiny.fetch(combModel.getLicences())) + .call(combModel -> { + if (c.getBanMembre() == null) + c.setBanMembre(new ArrayList<>()); + c.getBanMembre().remove(combModel.getId()); + return Panache.withTransaction(() -> repository.persist(c)); + }) + .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)) + .invoke(Unchecked.consumer(model -> { + if (data.getCategorie() == null) + throw new DBadRequestException(trad.t("categorie.requise")); + model.setCategorie(data.getCategorie()); + + if (data.getGenre() == null) { + if (model.getGenre() == null) + data.setGenre(Genre.NA); + } else + model.setGenre(data.getGenre()); + + if (data.getClub() == null) { + if (model.getClub() == null) + data.setClub(""); + } else + model.setClub(data.getClub()); + + if (data.getCountry() == null) { + if (model.getCountry() == null) + data.setCountry("FR"); + } else + model.setCountry(data.getCountry()); + + if (model.getCompetition().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(), + data.getCategoriesInscrites()) + .invoke(cats -> { + g.getCategoriesInscrites().clear(); + g.getCategoriesInscrites().addAll(cats); + g.getCategoriesInscrites().removeIf(cat -> cat.getCategories().stream() + .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())) + .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites())); + } } private Uni updateRegister(RegisterRequestData data, CompetitionModel c, - MembreModel combModel, boolean admin) { + MembreModel combModel, boolean admin, boolean append) { return registerRepository.find("competition = ?1 AND membre = ?2", c, combModel).firstResult() .onFailure().recoverWithNull() .map(Unchecked.function(r -> { if (r != null) { if (!admin && r.isLockEdit()) throw new DForbiddenException(trad.t("insc.err3")); - r.setWeight(data.getWeight()); - r.setOverCategory(data.getOverCategory()); + if (data.getOverCategory() != null || !append) + if (data.getOverCategory() == null) + r.setOverCategory(0); + else + r.setOverCategory(data.getOverCategory()); r.setCategorie( (combModel.getBirth_date() == null) ? combModel.getCategorie() : Utils.getCategoryFormBirthDate(combModel.getBirth_date(), @@ -395,34 +545,75 @@ public class CompetitionService { int days = Utils.getDaysBeforeCompetition(c.getDate()); if (days > -7) r.setClub(combModel.getClub()); - if (admin) + if (c.getRequiredWeight().contains(r.getCategorie2())) + if (data.getCountry() != null || !append) + r.setWeight(data.getWeight()); + if (admin) { + r.setWeightReal(data.getWeightReal()); r.setLockEdit(data.isLockEdit()); + } } else { r = new RegisterModel(c, combModel, data.getWeight(), data.getOverCategory(), (combModel.getBirth_date() == null) ? combModel.getCategorie() : Utils.getCategoryFormBirthDate(combModel.getBirth_date(), c.getDate()), (combModel.getClub() == null) ? null : combModel.getClub()); - if (admin) + if (admin) { + r.setWeightReal(data.getWeightReal()); r.setLockEdit(data.isLockEdit()); - else + } else r.setLockEdit(false); } if (c.getSystem() == CompetitionSystem.SAFCA) { SReqRegister.sendIfNeed(serverCustom.clients, new CompetitionData.SimpleRegister(r.getMembre().getId(), - r.getOverCategory(), r.getWeight(), r.getCategorie(), + r.getOverCategory(), r.getWeight2(), r.getCategorie(), (r.getClub() == null) ? null : r.getClub().getId(), (r.getClub() == null) ? null : r.getClub().getName()), c.getId()); } return r; })) + .call(r -> Mutiny.fetch(r.getCategoriesInscrites()).chain(__ -> + catPresetRepository.list("competition = ?1 AND id IN ?2", c, data.getCategoriesInscrites()) + .invoke(cats -> { + if (data.isQuick()) { + cats.removeIf(cat -> r.getCategoriesInscrites().stream() + .anyMatch(cp -> cp.equals(cat))); + } else { + r.getCategoriesInscrites().clear(); + } + r.getCategoriesInscrites().addAll(cats); + r.getCategoriesInscrites() + .removeIf(cat -> cat.getCategories().stream() + .noneMatch(e -> e.getCategorie().equals(r.getCategorie2()))); + }))) .chain(r -> Panache.withTransaction(() -> registerRepository.persist(r))) .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ? sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()); } + private Uni findGuestOrInit(String fname, String lname, CompetitionModel competition) { + if (fname == null || lname == null) + return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis"))); + return competitionGuestRepository.find( + "unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2) AND competition = ?3", + lname, fname, competition).firstResult() + .map(guestModel -> { + if (guestModel == null) { + CompetitionGuestModel model = new CompetitionGuestModel(); + model.setFname(fname); + if (lname.equals("__team")) + model.setLname("_team"); + else + model.setLname(lname); + model.setCompetition(competition); + return model; + } + return guestModel; + }); + } + private Uni findComb(Long licence, String fname, String lname) { if (licence != null && licence > 0) { return combRepository.find("licence = ?1", licence).firstResult() @@ -660,6 +851,12 @@ public class CompetitionService { .call(__ -> cache.invalidate(data.getId())); } + public Uni> getPresetsForCompetition(SecurityCtx securityCtx, Long id) { + return permService.hasViewPerm(securityCtx, id) + .chain(cm -> Mutiny.fetch(cm.getCatPreset())) + .map(p -> p.stream().map(PresetData::fromModel).toList()); + } + public Uni unregisterHelloAsso(NotificationData data) { if (!data.getState().equals("Refunded")) return Uni.createFrom().item(Response.ok().build()); @@ -693,8 +890,8 @@ public class CompetitionService { public Uni registerHelloAsso(NotificationData data) { String organizationSlug = data.getOrganizationSlug(); String formSlug = data.getFormSlug(); - RegisterRequestData req = new RegisterRequestData(null, "", "", null, 0, false, null, Categorie.CADET, Genre.NA, - null, "fr"); + RegisterRequestData req = new RegisterRequestData(null, "", "", null, null, 0, false, new ArrayList<>(), null, + Categorie.CADET, Genre.NA, null, "fr", false); return repository.find("data1 = ?1 AND data2 = ?2", organizationSlug, formSlug).firstResult() .onFailure().recoverWithNull() @@ -728,7 +925,7 @@ public class CompetitionService { .call(m -> Panache.withTransaction(() -> helloAssoRepository.persist( new HelloAssoRegisterModel(cm, m, data.getId())))) - .chain(m -> updateRegister(req, cm, m, true))) + .chain(m -> updateRegister(req, cm, m, true, true))) .onFailure().recoverWithItem(throwable -> { fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(), item.getUser().getFirstName(), optional.get())); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java index 63cdfd5..0d66050 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -381,11 +381,15 @@ public class KeycloakService { } public Optional getUserById(String userId) { - UserResource user = keycloak.realm(realm).users().get(userId); - if (user == null) + try { + UserResource user = keycloak.realm(realm).users().get(userId); + if (user == null) + return Optional.empty(); + else + return Optional.of(user.toRepresentation()); + } catch (Exception e) { return Optional.empty(); - else - return Optional.of(user.toRepresentation()); + } } private String makeLogin(MembreModel model) { diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index 1f61f08..024fe66 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -610,7 +610,7 @@ public class ResultService { } else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) { stat.w++; stat.win_ids.add(matchModel.getId()); - stat.score += 3; + stat.score += 2; } else { stat.l++; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java index 77cabc5..5a21ab2 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java @@ -1,10 +1,7 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.domain.service.CompetitionService; -import fr.titionfire.ffsaf.rest.data.CompetitionData; -import fr.titionfire.ffsaf.rest.data.RegisterRequestData; -import fr.titionfire.ffsaf.rest.data.SimpleCompetData; -import fr.titionfire.ffsaf.rest.data.SimpleRegisterComb; +import fr.titionfire.ffsaf.rest.data.*; import fr.titionfire.ffsaf.utils.SecurityCtx; import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; @@ -53,6 +50,16 @@ public class CompetitionEndpoints { return service.addRegisterComb(securityCtx, id, data, source); } + @POST + @Path("{id}/registers/{source}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Operation(hidden = true) + public Uni> addRegistersComb(@PathParam("id") Long id, @PathParam("source") String source, + List data) { + return service.addRegistersComb(securityCtx, id, data, source); + } + @DELETE @Path("{id}/register/{comb_id}/{source}") @Authenticated @@ -79,6 +86,13 @@ public class CompetitionEndpoints { return service.getInternalData(securityCtx, id); } + @GET + @Path("{id}/categories") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni> getPresetsForCompetition(@PathParam("id") Long id) { + return service.getPresetsForCompetition(securityCtx, id); + } @GET @Path("all") diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java index 15cf538..df37ffe 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.rest.data; +import fr.titionfire.ffsaf.data.model.CatPresetModel; import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; import fr.titionfire.ffsaf.data.model.CompetitionModel; import fr.titionfire.ffsaf.data.model.RegisterModel; @@ -10,6 +11,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Stream; @@ -33,7 +35,7 @@ public class CompetitionData { private Long club; private String clubName; private String owner; - private List registers; + private List registers; // for SAFCA private boolean canEdit; private boolean canEditRegisters; private String data1; @@ -41,6 +43,15 @@ public class CompetitionData { private String data3; private String data4; private String config; + private List presets; + private List requiredWeight; + + public CompetitionData() { + this(null, "", "", "", "", new Date(), new Date(), + CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true, + null, "", "", null, true, true, + "", "", "", "", "{}", new ArrayList<>(), new ArrayList<>()); + } public static CompetitionData fromModel(CompetitionModel model) { if (model == null) @@ -50,7 +61,8 @@ public class CompetitionData { model.getUuid(), model.getDate(), model.getTodate(), model.getSystem(), model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(), model.getClub().getId(), model.getClub().getName(), model.getOwner(), null, false, false, - model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig()); + model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig(), + new ArrayList<>(), model.getRequiredWeight()); } public static CompetitionData fromModelLight(CompetitionModel model) { @@ -61,7 +73,7 @@ public class CompetitionData { model.getAdresse(), "", model.getDate(), model.getTodate(), null, model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(), null, model.getClub().getName(), "", null, false, false, - "", "", "", "", "{}"); + "", "", "", "", "{}", new ArrayList<>(), model.getRequiredWeight()); if (model.getRegisterMode() == RegisterMode.HELLOASSO) { out.setData1(model.getData1()); @@ -75,15 +87,20 @@ public class CompetitionData { public CompetitionData addInsc(List insc, List guests) { this.registers = Stream.concat( insc.stream() - .map(i -> new SimpleRegister(i.getMembre().getId(), i.getOverCategory(), i.getWeight(), + .map(i -> new SimpleRegister(i.getMembre().getId(), i.getOverCategory(), i.getWeight2(), i.getCategorie(), (i.getClub() == null) ? null : i.getClub().getId(), (i.getClub() == null) ? null : i.getClub().getName())), guests.stream() - .map(i -> new SimpleRegister(i.getId() * -1, 0, i.getWeight(), + .map(i -> new SimpleRegister(i.getId() * -1, 0, i.getWeight2(), i.getCategorie(), null, i.getClub()))).toList(); return this; } + public CompetitionData addPresets(List presets) { + this.presets = presets.stream().map(PresetData::fromModel).toList(); + return this; + } + @Data @AllArgsConstructor @RegisterForReflection diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java new file mode 100644 index 0000000..60ec743 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java @@ -0,0 +1,29 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.CatPresetModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class PresetData { + private Long id; + private String name; + private CatPresetModel.SwordType sword; + private CatPresetModel.ShieldType shield; + private List categories; + private int mandatoryProtection1; + private int mandatoryProtection2; + + public static PresetData fromModel(CatPresetModel model) { + if (model == null) + return null; + + return new PresetData(model.getId(), model.getName(), model.getSwordType(), model.getShieldType(), + model.getCategories(), model.getMandatoryProtection1(), model.getMandatoryProtection2()); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java index 3b482f1..93d20e5 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -7,6 +7,8 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + @Data @AllArgsConstructor @NoArgsConstructor @@ -17,8 +19,10 @@ public class RegisterRequestData { private String lname; private Integer weight; - private int overCategory; + private Integer weightReal; + private Integer overCategory; private boolean lockEdit = false; + private List categoriesInscrites; // for guest registration only private Long id = null; @@ -26,4 +30,6 @@ public class RegisterRequestData { private Genre genre = Genre.NA; private String club = null; private String country = null; + + private boolean quick = false; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java index 148c950..0dd5799 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java @@ -1,9 +1,6 @@ package fr.titionfire.ffsaf.rest.data; -import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; -import fr.titionfire.ffsaf.data.model.LicenceModel; -import fr.titionfire.ffsaf.data.model.MembreModel; -import fr.titionfire.ffsaf.data.model.RegisterModel; +import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; import fr.titionfire.ffsaf.utils.Categorie; import fr.titionfire.ffsaf.utils.Genre; @@ -11,11 +8,14 @@ import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.ArrayList; import java.util.List; @Data @AllArgsConstructor +@NoArgsConstructor @RegisterForReflection public class SimpleRegisterComb { private long id; @@ -27,9 +27,11 @@ public class SimpleRegisterComb { private SimpleClubModel club; private Integer licence; private Integer weight; + private Integer weightReal; private int overCategory; private boolean hasLicenceActive; private boolean lockEdit; + private List categoriesInscrites; public static SimpleRegisterComb fromModel(RegisterModel register, List licences) { MembreModel membreModel = register.getMembre(); @@ -37,15 +39,21 @@ public class SimpleRegisterComb { membreModel.getGenre(), membreModel.getCountry(), (register.getCategorie() == null) ? null : register.getCategorie(), SimpleClubModel.fromModel(register.getClub()), membreModel.getLicence(), register.getWeight(), - register.getOverCategory(), + register.getWeightReal(), register.getOverCategory(), licences.stream().anyMatch(l -> l.isValidate() && l.getSaison() == Utils.getSaison()), - register.isLockEdit()); + register.isLockEdit(), new ArrayList<>()); } public static SimpleRegisterComb fromModel(CompetitionGuestModel guest) { return new SimpleRegisterComb(guest.getId() * -1, guest.getFname(), guest.getLname(), guest.getGenre(), guest.getCountry(), guest.getCategorie(), new SimpleClubModel(null, guest.getClub(), "fr", null), - null, guest.getWeight(), 0, false, false); + null, guest.getWeight(), guest.getWeightReal(), 0, false, false, + new ArrayList<>()); + } + + public SimpleRegisterComb setCategorieInscrite(List presets) { + this.categoriesInscrites = presets.stream().map(CatPresetModel::getId).toList(); + return this; } } diff --git a/src/main/resources/lang/messages_fr.properties b/src/main/resources/lang/messages_fr.properties index 54f6d55..a24c844 100644 --- a/src/main/resources/lang/messages_fr.properties +++ b/src/main/resources/lang/messages_fr.properties @@ -44,8 +44,8 @@ service.momentanement.indisponible=Service momentan asso.introuvable=Association introuvable erreur.lors.calcul.du.trie=Erreur lors du calcul du tri page.out.of.range=Page out of range -le.membre.appartient.pas.a.votre.club=Le membre n°%d n?appartient pas à votre club -email.deja.utilise.par=L?adresse e-mail '%s' est déjà utilisée par %s %s +le.membre.appartient.pas.a.votre.club=Le membre n°%d n'appartient pas à votre club +email.deja.utilise.par=L'adresse e-mail '%s' est déjà utilisée par %s %s try.edit.licence=Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (Tentative de modification non autorisée du nom sur la licence %d pour %s %s) email.deja.utilise=Adresse e-mail déjà utilisée regiter.new.membre=Pour enregistrer un nouveau membre, veuillez utiliser le bouton prévu à cet effet. @@ -61,23 +61,23 @@ licence.rm.err1=Impossible de supprimer une licence pour laquelle un paiement es licence.deja.demandee=Licence déjà demandée impossible.de.supprimer.une.licence.deja.validee=Impossible de supprimer une licence déjà validée impossible.de.supprimer.une.licence.deja.payee=Impossible de supprimer une licence déjà payée -vous.ne.pouvez.pas.creer.de.competition=Vous n?êtes pas autorisé à créer une compétition +vous.ne.pouvez.pas.creer.de.competition=Vous n'êtes pas autorisé à créer une compétition user.not.found=Utilisateur %s introuvable inscription.fermee=Inscription fermée -insc.err1=Vous n?êtes pas autorisé à inscrire ce membre (décision de l?administrateur de la compétition) -insc.err2=Vous n?êtes pas autorisé à vous inscrire (décision de l?administrateur de la compétition) -insc.err3=Modification bloquée par l?administrateur de la compétition +insc.err1=Vous n'êtes pas autorisé à inscrire ce membre (décision de l'administrateur de la compétition) +insc.err2=Vous n'êtes pas autorisé à vous inscrire (décision de l'administrateur de la compétition) +insc.err3=Modification bloquée par l'administrateur de la compétition licence.non.trouve=Licence %s introuvable nom.et.prenom.requis=Nom et prénom obligatoires combattant.non.trouve=Combattant %s %s introuvable -le.membre.n.existe.pas=Le membre n°%d n?existe pas +le.membre.n.existe.pas=Le membre n°%d n'existe pas competition.is.not.internal=Competition is not INTERNAL erreur.de.format.des.contacts=Format des contacts invalide competition.not.found=Compétition introuvable saison.non.valid=Saison invalide -demande.d.affiliation.deja.existante=Une demande d?affiliation existe déjà +demande.d.affiliation.deja.existante=Une demande d'affiliation existe déjà affiliation.deja.existante=Affiliation déjà existante licence.membre.n.1.inconnue=Licence du membre n°1 inconnue licence.membre.n.2.inconnue=Licence du membre n°2 inconnue licence.membre.n.3.inconnue=Licence du membre n°3 inconnue -demande.d.affiliation.non.trouve=Demande d?affiliation introuvable \ No newline at end of file +demande.d.affiliation.non.trouve=Demande d'affiliation introuvable \ No newline at end of file diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 820f7b9..dcab915 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -18,7 +18,7 @@ "@fortawesome/react-fontawesome": "^3.1.1", "axios": "^1.13.2", "browser-image-compression": "^2.0.2", - "i18next": "^25.7.4", + "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", @@ -33,12 +33,12 @@ "react-loader-spinner": "^8.0.2", "react-router-dom": "^7.12.0", "react-toastify": "^11.0.5", - "recharts": "^3.6.0", - "xlsx": "^0.18.5", + "recharts": "^3.7.0", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xlsx-js-style": "^1.2.0" }, "devDependencies": { - "@types/react": "^19.2.8", + "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", "eslint": "^9.39.2", @@ -1606,9 +1606,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "devOptional": true, "license": "MIT", "peer": true, @@ -2048,14 +2048,6 @@ "node": ">=6" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3357,9 +3349,9 @@ } }, "node_modules/i18next": { - "version": "25.7.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz", - "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==", + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz", + "integrity": "sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==", "funding": [ { "type": "individual", @@ -3374,6 +3366,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" @@ -4674,9 +4667,13 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/recharts": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", - "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", @@ -5735,18 +5732,10 @@ } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "license": "Apache-2.0", "bin": { "xlsx": "bin/xlsx.njs" }, diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index d06dcd3..6575d02 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -20,7 +20,7 @@ "@fortawesome/react-fontawesome": "^3.1.1", "axios": "^1.13.2", "browser-image-compression": "^2.0.2", - "i18next": "^25.7.4", + "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", @@ -35,12 +35,12 @@ "react-loader-spinner": "^8.0.2", "react-router-dom": "^7.12.0", "react-toastify": "^11.0.5", - "recharts": "^3.6.0", - "xlsx": "^0.18.5", + "recharts": "^3.7.0", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xlsx-js-style": "^1.2.0" }, "devDependencies": { - "@types/react": "^19.2.8", + "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", "eslint": "^9.39.2", diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json index 5e0df67..c993b44 100644 --- a/src/main/webapp/public/locales/en/cm.json +++ b/src/main/webapp/public/locales/en/cm.json @@ -102,6 +102,7 @@ "supprimer": "Delete", "sélectionneLesModesDaffichage": "Select display modes", "sélectionner": "Select", + "team": "Team", "terminé": "Finished", "texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.", "toast.createCategory.error": "Error while creating the category", @@ -113,6 +114,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.team.update.error": "Error while updating team", + "toast.team.update.pending": "Updating team...", + "toast.team.update.success": "Team updated!", "toast.updateCategory.error": "Error while updating the category", "toast.updateCategory.pending": "Updating category...", "toast.updateCategory.success": "Category updated!", @@ -125,12 +129,8 @@ "toast.updateTrees.init.success": "Trees created!", "toast.updateTrees.pending": "Updating tournament trees...", "toast.updateTrees.success": "Trees updated!", - "toast.team.update.error": "Error while updating team", - "toast.team.update.pending": "Updating team...", - "toast.team.update.success": "Team updated!", "tournoi": "Tournament", "tournois": "Tournaments", - "team": "Team", "tousLesMatchs": "All matches", "toutConserver": "Keep all", "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json index 50fe38a..f7f7f12 100644 --- a/src/main/webapp/public/locales/en/common.json +++ b/src/main/webapp/public/locales/en/common.json @@ -1,6 +1,7 @@ { "(optionnelle)": "(optional)", "---SansClub---": "--- no club ---", + "---TousLesAges---": "--- all ages ---", "---ToutLesClubs---": "--- all clubs ---", "---ToutLesPays---": "--- all countries ---", "---TouteLesCatégories---": "--- all categories ---", @@ -8,6 +9,7 @@ "--SélectionnerCatégorie--": "-- Select category --", "1Catégorie": "+1 category", "2Catégorie": "+2 categories", + "LesModificationsNontEnregistrer": "/!\\ The changes have not yet been saved, click save /!\\", "activer": "Activate", "admin": "Administration", "administrateur": "Administrator", @@ -74,6 +76,7 @@ "aff_req.toast.undo.error": "Failed to cancel affiliation request", "aff_req.toast.undo.pending": "Cancelling affiliation request in progress", "aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉", + "afficherLesCombattantsNonPesés": "Show unweighed fighters", "afficherLétatDesAffiliation": "Display affiliation status", "affiliation": "Affiliation", "affiliationNo": "Affiliation no. {{no}}", @@ -81,11 +84,15 @@ "ajouterUnClub": "Add a club", "ajouterUnMembre": "Add a member", "all_season": "--- all seasons ---", + "ans": "years", + "arme": "Weapon", "au": "to", "aucun": "None", "aucunMembreSélectionné": "No member selected", + "aucuneCatégorieDisponible": "No categories available at this time.", "back": "« back", "blason": "Coat of arms", + "bouclier": "Shield", "bureau": "Board", "button.accepter": "Accept", "button.ajouter": "Add", @@ -93,7 +100,6 @@ "button.appliquer": "Apply", "button.confirmer": "Confirm", "button.créer": "Create", - "button.enregister": "Save", "button.enregistrer": "Save", "button.fermer": "Close", "button.modifier": "Edit", @@ -101,6 +107,7 @@ "button.seDésinscrire": "Unsubscribe", "button.suivant": "Next", "button.supprimer": "Delete", + "casque": "Helmet", "cat.benjamin": "Benjamin", "cat.cadet": "Cadet", "cat.catégorieInconnue": "Unknown category", @@ -115,7 +122,9 @@ "cat.vétéran2": "Veteran 2", "categorie": "category", "catégorie": "Category", + "catégorieàAjouter": "Category to add", "certificatMédical": "Medical certificate", + "champAttendu": "Expected field", "chargement...": "Loading...", "chargerLexcel": "Load Excel", "chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.", @@ -146,6 +155,7 @@ "club_one": "Club", "club_other": "Clubs", "club_zero": "No club", + "colonneDansLeFichier": "Column in the file", "combattant": "fighter", "comp.aff.blason": "Display the club's coat of arms on screens", "comp.aff.flag": "Display the fighter's country on screens", @@ -186,7 +196,9 @@ "comp.inscriptionsParLesAdministrateursDeLaCompétition": "Registrations by competition administrators", "comp.inscriptionsParLesResponsablesDeClub": "Registrations by club managers", "comp.inscriptionsSurLaBilletterieHelloasso": "Registrations on the HelloAsso ticketing", + "comp.modal.annoncé": "Announced", "comp.modal.information": "Information", + "comp.modal.pesé": "Weighed", "comp.modal.poids": "Weight (in kg)", "comp.modal.recherche": "Search*", "comp.modal.surclassement": "Overclassification", @@ -221,6 +233,8 @@ "comp.toast.register.add.error": "Fighter not found", "comp.toast.register.add.pending": "Search in progress", "comp.toast.register.add.success": "Fighter found and added/updated", + "comp.toast.register.addMultiple.success_one": "Successful import for 1 fighter", + "comp.toast.register.addMultiple.success_other": "Successful import for {{count}} fighters", "comp.toast.register.ban.error": "Error", "comp.toast.register.ban.pending": "Unregistration in progress", "comp.toast.register.ban.success": "Fighter unregistered and banned", @@ -249,11 +263,13 @@ "compte": "Account", "compétition": "Competition", "configuration": "Configuration", + "configurationDeLaCatégorie": "Category configuration", "conserverLancienEmail": "Keep the old email", "contactAdministratif": "Administrative contact", "contactInterne": "Internal contact", "contact_one": "Contact", "contact_other": "Contacts", + "coquilleProtectionPelvienne": "Shell / Pelvic protection", "date": "Date", "dateDeNaissance": "Date of birth", "days": [ @@ -274,6 +290,8 @@ "donnéesAdministratives": "Administrative data", "du": "From", "dun": "of a", + "duréePause": "Pause duration", + "duréeRound": "Round duration", "définirLidDuCompte": "Define account ID", "editionDeL'affiliation": "Editing affiliation", "editionDeLaDemande": "Editing request", @@ -286,13 +304,18 @@ "erreurDePaiement": "Payment error😕", "erreurDePaiement.detail": "Error message:", "erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.", + "erreurPourLinscription": "Registration error", "espaceAdministration": "Administration space", "f": "F", "faitPar": "Done by", "femme": "Female", "filtre": "Filter", + "gantMainBouclier": "Shield hand glove", + "gantMainsArmées": "Armed hand(s) glove(s)", + "gants": "Gloves", "genre": "Gender", "gestionGroupée": "Group management", + "gorgerin": "Gorgerin", "gradeDarbitrage": "Refereeing grade", "h": "M", "home": { @@ -304,6 +327,9 @@ }, "homme": "Male", "horairesD'entraînements": "Training schedules", + "importationDuFichier": "Importing the file", + "importerDesCombattants": "Import fighters", + "importerDesInvités": "Import guests", "information": "Information", "invité": "guest", "keepEmpty": "Leave blank to make no changes.", @@ -312,6 +338,8 @@ "licenceNo": "License no. {{no}}", "lieu": "Place", "lieuxDentraînements": "Training locations", + "ligneIgnorée1": "Line ignored: missing name, first name or category.", + "ligneIgnorée2": "Line ignored: missing first name or license.", "loading": "Loading...", "me": { "result": { @@ -440,6 +468,8 @@ "nouveauClub": "New club", "nouveauMembre": "New member", "nouvelEmail": "New email", + "numéroDeLaLigneDentête": "Header line number", + "numéroDeLigne": "Line number", "ou": "or", "oui": "Yes", "outdated_session": { @@ -472,8 +502,21 @@ "perm.créerDesCompétion": "Create competitions", "perm.ffsafIntra": "FFSAF intra", "permission": "Permission", + "peutSinscrire": "Can register?", "photos": "Photos", + "plastron": "Breastplate", + "poids": "Weight", + "poidsDemandéPour": "Weight required for", "prenom": "First name", + "protectionDeBras": "Arm protection", + "protectionDeBrasArmé": "Protection of armed arm(s)", + "protectionDeBrasDeBouclier": "Shield arm protection", + "protectionDeCoudes": "Elbow protection", + "protectionDeGenoux": "Knee protection", + "protectionDeJambes": "Leg protection", + "protectionDePieds": "Foot protection", + "protectionDorsale": "Back protector", + "protectionObligatoire": "Mandatory protection", "prénomEtNom": "First and last name", "rechercher": "Search", "rechercher...": "Search...", @@ -494,8 +537,15 @@ "role.vise-secrétaire": "Vice-Secretary", "role.vise-trésorier": "Vice-Treasurer", "saison": "Season", + "sans": "Without", "secrétariatsDeLice": "Ring secretariats", "selectionner...": "Select...", + "shield.buckler": "Buckler", + "shield.none": "$t(sans) / $t(nonDéfinie)", + "shield.round": "Round", + "shield.standard": "Standard", + "shield.teardrop": "Teardrop", + "siDisponiblePourLaCatégorieDages": "If available for the age category", "siretOuRna": "SIRET or RNA", "stats": "Statistics", "statue": "Statue", @@ -505,6 +555,10 @@ "supprimerLeClub.msg": "Are you sure you want to delete this club?", "supprimerLeCompte": "Delete account", "supprimerLeCompte.msg": "Are you sure you want to delete this account?", + "sword.none": "$t(sans) / $t(nonDéfinie)", + "sword.oneHand": "One hand", + "sword.saber": "Saber", + "sword.twoHand": "Two hands", "sélectionEnéquipeDeFrance": "Selection in the French team", "sélectionner...": "Select...", "toast.edit.error": "Failed to save changes", @@ -536,8 +590,75 @@ "validerLicence_other": "Validate the {{count}} selected licenses", "validerLicence_zero": "$t(validerLicence_other)", "validée": "Validated", + "veuillezAssocierChaqueChampàUneColonneDuFichier": "Please associate each field with a column in the file", + "veuillezIndiqueràQuelle": "Please indicate on which line the headers are located in the file", + "veuillezMapperLesColonnesSuivantes": "Please map the following columns", "voir/modifierLesParticipants": "View/Edit participants", "voirLesStatues": "View statues", + "vousNêtesPasEncoreInscrit": "You are not yet registered or your registration has not yet been entered on the intranet", "à": "at", - "étatDeLaDemande": "Request status" + "étatDeLaDemande": "Request status", + "fileImport.variants": { + "licence": [ + "license", + "licence", + "license number", + "license ID", + "ID license", + "licence no" + ], + "pays": [ + "country", + "pays", + "country of residence", + "origin country" + ], + "nom": [ + "last name", + "nom", + "family name", + "surname", + "lastname" + ], + "prenom": [ + "first name", + "prénom", + "given name", + "first given name" + ], + "genre": [ + "gender", + "genre", + "sex", + "civility" + ], + "weight": [ + "weight", + "poids", + "weight (kg)", + "actual weight", + "mass" + ], + "categorie": [ + "category", + "catégorie", + "weight category", + "age category" + ], + "overCategory": [ + "over category", + "surclassement", + "category override", + "over classification" + ], + "club": [ + "club", + "club name", + "association", + "association name" + ] + }, + "comp.toast.registers.addMultiple.error": "Import failed", + "comp.toast.registers.addMultiple.pending": "Import in progress", + "comp.toast.registers.addMultiple.success": "Import completed successfully 🎉" } diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json index 4cc89af..94153d3 100644 --- a/src/main/webapp/public/locales/fr/cm.json +++ b/src/main/webapp/public/locales/fr/cm.json @@ -102,6 +102,7 @@ "supprimer": "Supprimer", "sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage", "sélectionner": "Sélectionner", + "team": "Équipe", "terminé": "Terminé", "texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.", "toast.createCategory.error": "Erreur lors de la création de la catégorie", @@ -113,6 +114,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.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 !", "toast.updateCategory.error": "Erreur lors de la mise à jour de la catégorie", "toast.updateCategory.pending": "Mise à jour de la catégorie...", "toast.updateCategory.success": "Catégorie mise à jour !", @@ -125,12 +129,8 @@ "toast.updateTrees.init.success": "Arbres créés !", "toast.updateTrees.pending": "Mise à jour des arbres du tournoi...", "toast.updateTrees.success": "Arbres mis à jour !", - "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 !", "tournoi": "Tournoi", "tournois": "Tournois", - "team": "Équipe", "tousLesMatchs": "Tous les matchs", "toutConserver": "Tout conserver", "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", diff --git a/src/main/webapp/public/locales/fr/common.json b/src/main/webapp/public/locales/fr/common.json index d8b246d..8a11036 100644 --- a/src/main/webapp/public/locales/fr/common.json +++ b/src/main/webapp/public/locales/fr/common.json @@ -1,13 +1,15 @@ { "(optionnelle)": "(optionnelle)", "---SansClub---": "--- sans club ---", - "---ToutLesClubs---": "--- tout les clubs ---", + "---TousLesAges---": "--- tous les ages ---", + "---ToutLesClubs---": "--- tous les clubs ---", "---ToutLesPays---": "--- tout les pays ---", "---TouteLesCatégories---": "--- toute les catégories ---", "--NonLicencier--": "-- Non licencier --", "--SélectionnerCatégorie--": "-- Sélectionner catégorie --", "1Catégorie": "+1 catégorie", "2Catégorie": "+2 catégorie", + "LesModificationsNontEnregistrer": "/!\\ Les modifications n'ont pas encore été enregistré, cliqué sur enregistrer /!\\", "activer": "Activer", "admin": "Administration", "administrateur": "Administrateur", @@ -74,6 +76,7 @@ "aff_req.toast.undo.error": "Échec de l'annulation de la demande d'affiliation", "aff_req.toast.undo.pending": "Annulation de la demande d'affiliation en cours", "aff_req.toast.undo.success": "Demande d'affiliation annulée avec succès 🎉", + "afficherLesCombattantsNonPesés": "Afficher les combattants non pesés", "afficherLétatDesAffiliation": "Afficher l'état des affiliation", "affiliation": "Affiliation", "affiliationNo": "Affiliation n°{{no}}", @@ -81,11 +84,15 @@ "ajouterUnClub": "Ajouter un club", "ajouterUnMembre": "Ajouter un membre", "all_season": "--- tout les saisons ---", + "ans": "ans", + "arme": "Arme", "au": "au", "aucun": "Aucun", "aucunMembreSélectionné": "Aucun membre sélectionné", + "aucuneCatégorieDisponible": "Aucune catégorie disponible pour le moment.", "back": "« retour", "blason": "Blason", + "bouclier": "Bouclier", "bureau": "Bureau", "button.accepter": "Accepter", "button.ajouter": "Ajouter", @@ -93,7 +100,6 @@ "button.appliquer": "Appliquer", "button.confirmer": "Confirmer", "button.créer": "Créer", - "button.enregister": "Enregister", "button.enregistrer": "Enregistrer", "button.fermer": "Fermer", "button.modifier": "Modifier", @@ -101,6 +107,7 @@ "button.seDésinscrire": "Se désinscrire", "button.suivant": "Suivant", "button.supprimer": "Supprimer", + "casque": "Casque", "cat.benjamin": "Benjamin", "cat.cadet": "Cadet", "cat.catégorieInconnue": "Catégorie inconnue", @@ -115,7 +122,9 @@ "cat.vétéran2": "Vétéran 2", "categorie": "categorie", "catégorie": "Catégorie", + "catégorieàAjouter": "Catégorie à ajouter", "certificatMédical": "Certificat médical", + "champAttendu": "Champ attendu", "chargement...": "Chargement...", "chargerLexcel": "Charger l'Excel", "chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.", @@ -146,6 +155,7 @@ "club_one": "Club", "club_other": "Clubs", "club_zero": "Sans club", + "colonneDansLeFichier": "Colonne dans le fichier", "combattant": "combattant", "comp.aff.blason": "Afficher le blason du club sur les écrans", "comp.aff.flag": "Afficher le pays du combattant sur les écrans", @@ -165,7 +175,7 @@ "comp.error1": "La date de fin doit être postérieure à la date de début.", "comp.error2": "Veuillez renseigner les dates de début et de fin d'inscription.", "comp.error3": "La date de fin d'inscription doit être postérieure à la date de début d'inscription.", - "comp.exporterLesInscription": "Exporter les inscription", + "comp.exporterLesInscription": "Exporter les inscriptions", "comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email de réception des inscriptions échoué", "comp.ha.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.", "comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.", @@ -186,7 +196,9 @@ "comp.inscriptionsParLesAdministrateursDeLaCompétition": "Inscriptions par les administrateurs de la compétition", "comp.inscriptionsParLesResponsablesDeClub": "Inscriptions par les responsables de club", "comp.inscriptionsSurLaBilletterieHelloasso": "Inscriptions sur la billetterie HelloAsso", + "comp.modal.annoncé": "Annoncé", "comp.modal.information": "Information", + "comp.modal.pesé": "Pesé", "comp.modal.poids": "Poids (en kg)", "comp.modal.recherche": "Recherche*", "comp.modal.surclassement": "Surclassement", @@ -221,6 +233,8 @@ "comp.toast.register.add.error": "Combattant non trouvé", "comp.toast.register.add.pending": "Recherche en cours", "comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour", + "comp.toast.register.addMultiple.success_one": "Importation réussie pour 1 combattant", + "comp.toast.register.addMultiple.success_other": "Importation réussie pour {{count}} combattants", "comp.toast.register.ban.error": "Erreur", "comp.toast.register.ban.pending": "Désinscription en cours", "comp.toast.register.ban.success": "Combattant désinscrit et bannie", @@ -249,11 +263,13 @@ "compte": "Compte", "compétition": "Compétition", "configuration": "Configuration", + "configurationDeLaCatégorie": "Configuration de la catégorie", "conserverLancienEmail": "Conserver l'ancien email", "contactAdministratif": "Contact administratif", "contactInterne": "Contact interne", "contact_one": "Contact", "contact_other": "Contacts", + "coquilleProtectionPelvienne": "Coquille / Protection pelvienne", "date": "Date", "dateDeNaissance": "Date de naissance", "days": [ @@ -274,6 +290,8 @@ "donnéesAdministratives": "Données administratives", "du": "Du", "dun": "d'un", + "duréePause": "Durée pause", + "duréeRound": "Durée round", "définirLidDuCompte": "Définir l'id du compte", "editionDeL'affiliation": "Edition de l'affiliation", "editionDeLaDemande": "Edition de la demande ", @@ -286,13 +304,18 @@ "erreurDePaiement": "Erreur de paiement😕", "erreurDePaiement.detail": "Message d'erreur :", "erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.", + "erreurPourLinscription": "Erreur pour l'inscription", "espaceAdministration": "Espace administration", "f": "F", "faitPar": "Fait par", "femme": "Femme", "filtre": "Filtre", + "gantMainBouclier": "Gant main de bouclier", + "gantMainsArmées": "Gant main(s) armée(s)", + "gants": "Gants", "genre": "Genre", "gestionGroupée": "Gestion groupée", + "gorgerin": "Gorgerin", "gradeDarbitrage": "Grade d'arbitrage", "h": "H", "home": { @@ -304,6 +327,9 @@ }, "homme": "Homme", "horairesD'entraînements": "Horaires d'entraînements", + "importationDuFichier": "Importation du fichier", + "importerDesCombattants": "Importer des combattants", + "importerDesInvités": "Importer des invités", "information": "Information", "invité": "invité", "keepEmpty": "Laissez vide pour ne rien changer.", @@ -312,6 +338,8 @@ "licenceNo": "Licence n°{{no}}", "lieu": "Lieu", "lieuxDentraînements": "Lieux d'entraînements", + "ligneIgnorée1": "Ligne ignorée : nom, prénom ou catégorie manquante.", + "ligneIgnorée2": "Ligne ignorée : nom prénom ou licence manquante.", "loading": "Chargement...", "me": { "result": { @@ -440,6 +468,8 @@ "nouveauClub": "Nouveau club", "nouveauMembre": "Nouveau membre", "nouvelEmail": "Nouvel email", + "numéroDeLaLigneDentête": "Numéro de la ligne d'en-tête", + "numéroDeLigne": "Numéro de ligne", "ou": "Ou", "oui": "Oui", "outdated_session": { @@ -472,8 +502,21 @@ "perm.créerDesCompétion": "Créer des compétion", "perm.ffsafIntra": "FFSAF intra", "permission": "Permission", + "peutSinscrire": "Peut s'inscrire?", "photos": "Photos", + "plastron": "Plastron", + "poids": "Poids", + "poidsDemandéPour": "Poids demandé pour", "prenom": "Prénom", + "protectionDeBras": "Protection de bras", + "protectionDeBrasArmé": "Protection de bras armé(s)", + "protectionDeBrasDeBouclier": "Protection de bras de bouclier", + "protectionDeCoudes": "Protection de coudes", + "protectionDeGenoux": "Protection de genoux", + "protectionDeJambes": "Protection de jambes", + "protectionDePieds": "Protection de pieds", + "protectionDorsale": "Protection dorsale", + "protectionObligatoire": "Protection obligatoire", "prénomEtNom": "Prénom et nom", "rechercher": "Rechercher", "rechercher...": "Rechercher...", @@ -494,8 +537,15 @@ "role.vise-secrétaire": "Vise-Secrétaire", "role.vise-trésorier": "Vise-Trésorier", "saison": "Saison", + "sans": "Sans", "secrétariatsDeLice": "Secrétariats de lice", "selectionner...": "Sélectionner...", + "shield.buckler": "Bocle", + "shield.none": "$t(sans) / $t(nonDéfinie)", + "shield.round": "Rond", + "shield.standard": "Standard", + "shield.teardrop": "Larme", + "siDisponiblePourLaCatégorieDages": "Si disponible pour la catégorie d'ages", "siretOuRna": "SIRET ou RNA", "stats": "Statistiques", "statue": "Statue", @@ -505,6 +555,10 @@ "supprimerLeClub.msg": "Êtes-vous sûr de vouloir supprimer ce club ?", "supprimerLeCompte": "Supprimer le compte", "supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?", + "sword.none": "$t(sans) / $t(nonDéfinie)", + "sword.oneHand": "Une main", + "sword.saber": "Sabre", + "sword.twoHand": "Deux mains", "sélectionEnéquipeDeFrance": "Sélection en équipe de France", "sélectionner...": "Sélectionner...", "toast.edit.error": "Échec de l'enregistrement des modifications", @@ -536,8 +590,79 @@ "validerLicence_other": "Valider les {{count}} licences sélectionnées", "validerLicence_zero": "$t(validerLicence_other)", "validée": "Validée", + "veuillezAssocierChaqueChampàUneColonneDuFichier": "Veuillez associer chaque champ à une colonne du fichier", + "veuillezIndiqueràQuelle": "Veuillez indiquer à quelle ligne se trouvent les en-têtes dans le fichier", + "veuillezMapperLesColonnesSuivantes": "Veuillez mapper les colonnes suivantes", "voir/modifierLesParticipants": "Voir/Modifier les participants", "voirLesStatues": "Voir les statues", + "vousNêtesPasEncoreInscrit": "Vous n'êtes pas encore inscrit ou votre inscription n'a pas encore été rentrée sur l'intranet", "à": "à", - "étatDeLaDemande": "État de la demande" + "étatDeLaDemande": "État de la demande", + "fileImport.variants": { + "licence": [ + "licence", + "n° licence", + "num licence", + "id licence", + "license", + "licence id" + ], + "pays": [ + "pays", + "country", + "pays de résidence", + "pays d'origine" + ], + "nom": [ + "nom", + "nom de famille", + "lastname", + "family name", + "nom complet" + ], + "prenom": [ + "prénom", + "prenom", + "first name", + "given name", + "prénom usuel" + ], + "genre": [ + "genre", + "sexe", + "gender", + "sex", + "civilité" + ], + "weight": [ + "poids", + "weight", + "poids (kg)", + "poids réel", + "masse" + ], + "categorie": [ + "catégorie", + "category", + "catégorie de poids", + "weight category", + "catégorie d'âge" + ], + "overCategory": [ + "surclassement", + "over category", + "surcatégorie", + "surclassement de catégorie" + ], + "club": [ + "club", + "nom du club", + "club name", + "association", + "nom de l'association" + ] + }, + "comp.toast.registers.addMultiple.error": "Erreur lors de l'importation des combattants", + "comp.toast.registers.addMultiple.pending": "Importation des combattants en cours...", + "comp.toast.registers.addMultiple.success": "Importation des combattants réussie 🎉" } diff --git a/src/main/webapp/src/Keycloak.js b/src/main/webapp/src/Keycloak.js deleted file mode 100644 index de4e422..0000000 --- a/src/main/webapp/src/Keycloak.js +++ /dev/null @@ -1,14 +0,0 @@ -import Keycloak from "keycloak-js"; - -const vite_url = import.meta.env.VITE_URL; -const client_id = import.meta.env.VITE_CLIENT_ID; - -const keycloak = new Keycloak({ - url: `${vite_url}/auth-api`, - realm: "safca", - clientId: client_id, -}); - - - -export default keycloak; \ No newline at end of file diff --git a/src/main/webapp/src/assets/CategoryPreset.js b/src/main/webapp/src/assets/CategoryPreset.js new file mode 100644 index 0000000..05c040b --- /dev/null +++ b/src/main/webapp/src/assets/CategoryPreset.js @@ -0,0 +1,115 @@ +const CategoryPreset = [ + { + name: "Épée", + sword: "ONE_HAND", + shield: "NONE", + categories: [ + {categorie: "MINI_POUSSIN", roundDuration: 30000, pauseDuration: 60000}, + {categorie: "POUSSIN", roundDuration: 30000, pauseDuration: 60000}, + {categorie: "BENJAMIN", roundDuration: 45000, pauseDuration: 60000}, + {categorie: "MINIME", roundDuration: 45000, pauseDuration: 60000}, + {categorie: "CADET", roundDuration: 60000, pauseDuration: 60000}, + ], + mandatoryProtection1: 45, + mandatoryProtection2: 13 + }, + { + name: "Épée Bouclier", + sword: "ONE_HAND", + shield: "STANDARD", + categories: [ + {categorie: "SUPER_MINI", roundDuration: 30000, pauseDuration: 60000}, + {categorie: "MINI_POUSSIN", roundDuration: 30000, pauseDuration: 60000}, + {categorie: "POUSSIN", roundDuration: 45000, pauseDuration: 60000}, + {categorie: "BENJAMIN", roundDuration: 45000, pauseDuration: 60000}, + {categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "CADET", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000}, + ], + mandatoryProtection1: 45, + mandatoryProtection2: 13 + }, + { + name: "Épée Bocle", + sword: "ONE_HAND", + shield: "BUCKLER", + categories: [ + {categorie: "POUSSIN", roundDuration: 45000, pauseDuration: 60000}, + {categorie: "BENJAMIN", roundDuration: 45000, pauseDuration: 60000}, + {categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "CADET", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000}, + ], + mandatoryProtection1: 45, + mandatoryProtection2: 13 + }, + { + name: "Épée Longue", + sword: "TWO_HAND", + shield: "NONE", + categories: [ + {categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000}, + ], + mandatoryProtection1: 61, + mandatoryProtection2: 29 + }, + { + name: "Sabre", + sword: "SABER", + shield: "NONE", + categories: [ + {categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "CADET", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000}, + ], + mandatoryProtection1: 47, + mandatoryProtection2: 47 + }, + { + name: "Sabre Bocle", + sword: "SABER", + shield: "BUCKLER", + categories: [ + {categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "CADET", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000}, + {categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000}, + ], + mandatoryProtection1: 47, + mandatoryProtection2: 47 + }, + { + name: "Profight Léger", + sword: "ONE_HAND", + shield: "TEARDROP", + categories: [ + {categorie: "SENIOR1", roundDuration: 120000, pauseDuration: 60000}, + {categorie: "SENIOR2", roundDuration: 120000, pauseDuration: 60000}, + {categorie: "VETERAN1", roundDuration: 120000, pauseDuration: 60000}, + {categorie: "VETERAN2", roundDuration: 120000, pauseDuration: 60000}, + ], + mandatoryProtection1: 3647, + mandatoryProtection2: 3647 + }, +] + +export default CategoryPreset; diff --git a/src/main/webapp/src/components/FileImport.jsx b/src/main/webapp/src/components/FileImport.jsx new file mode 100644 index 0000000..24f3552 --- /dev/null +++ b/src/main/webapp/src/components/FileImport.jsx @@ -0,0 +1,241 @@ +import React, {useId, useRef, useState} from "react"; +import {toast} from "react-toastify"; +import * as XLSX from "xlsx"; +import {useTranslation} from "react-i18next"; + +const parseValue = (value, type) => { + if (value === undefined || value === null) + return null; + + switch (type) { + case 'Integer': + if (value === '') + return null; + const parsedInt = parseInt(value, 10); + return isNaN(parsedInt) ? null : parsedInt; + case 'Boolean': + if (typeof value === 'boolean') + return value; + if (typeof value === 'string') { + const lowerValue = value.toLowerCase().trim(); + if (lowerValue === 'oui' || lowerValue === 'true' || lowerValue === '1' || lowerValue === 'x') { + return true; + } else if (lowerValue === 'non' || lowerValue === 'false' || lowerValue === '0' || lowerValue === '') { + return false; + } + } + return null; + case 'Date': + if (value === '') + return null; + if (typeof value === 'string') { + const date = new Date(value); + return isNaN(date.getTime()) ? null : date; + } + return null; + case 'String': + default: + return String(value); + } +}; + +export function FileImport({onDataMapped, expectedFields, textButton}) { + const id = useId(); + const [headerLineNumber, setHeaderLineNumber] = useState(1); + const [fileData, setFileData] = useState([]); + const [fileHeaders, setFileHeaders] = useState([]); + const [fileName, setFileName] = useState(''); + const [selectedFile, setSelectedFile] = useState(null); + const [columnMappings, setColumnMappings] = useState({}); + const fileChooser = useRef(null); + const openMappingModal = useRef(null); + const closeMappingModal = useRef(null); + const openHeaderLineModal = useRef(null); + const {t} = useTranslation(); + + + // Fonction pour trouver la meilleure correspondance + const findBestMatch = (fileHeaders, expectedField) => { + const fieldLabel = expectedField.label.toLowerCase(); + const fieldKey = expectedField.key.toLowerCase(); + + // Variantes possibles pour chaque champ (ex: "Nom" peut être "nom", "Nom de famille", etc.) + const variants = { + licence: t('fileImport.variants.licence', {returnObjects: true}), + pays: t('fileImport.variants.pays', {returnObjects: true}), + nom: t('fileImport.variants.nom', {returnObjects: true}), + prenom: t('fileImport.variants.prenom', {returnObjects: true}), + genre: t('fileImport.variants.genre', {returnObjects: true}), + weight: t('fileImport.variants.weight', {returnObjects: true}), + categorie: t('fileImport.variants.categorie', {returnObjects: true}), + overCategory: t('fileImport.variants.overCategory', {returnObjects: true}), + club: t('fileImport.variants.club', {returnObjects: true}), + }; + + // Recherche de la meilleure correspondance + for (const header of fileHeaders) { + const lowerHeader = header.toLowerCase(); + if (lowerHeader === fieldLabel || lowerHeader === fieldKey || (variants[fieldKey] && variants[fieldKey].includes(lowerHeader))) { + return header; + } + } + + // Aucune correspondance trouvée + return null; + }; + + // Gestion du fichier sélectionné + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (!file) return; + + setSelectedFile(file); + setFileName(file.name); + openHeaderLineModal.current.click(); + }; + + // Valider le numéro de la ligne d'en-tête et lire le fichier + const handleHeaderLineSubmit = () => { + if (!selectedFile) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const data = event.target.result; + const workbook = XLSX.read(data, {type: 'binary'}); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(sheet, {header: 1}); + + // Extraire les en-têtes et les données en fonction du numéro de ligne + const headers = jsonData[headerLineNumber - 1]; + const rows = jsonData.slice(headerLineNumber); + setFileHeaders(headers); + setFileData(rows); + + // Initialiser le mapping avec pré-remplissage intelligent + const initialMappings = {}; + expectedFields.forEach(field => { + const bestMatch = findBestMatch(headers, field); + initialMappings[field.key] = bestMatch || ''; + }); + setColumnMappings(initialMappings); + + openMappingModal.current.click(); + fileChooser.current.value = ''; + }; + reader.readAsBinaryString(selectedFile); + }; + + // Mettre à jour le mapping d'une colonne + const handleMappingChange = (fieldKey, header) => { + setColumnMappings({ + ...columnMappings, + [fieldKey]: header, + }); + }; + + // Valider le mapping et envoyer les données + const handleSubmit = () => { + // Vérifier que tous les champs requis sont mappés + const missingMappings = expectedFields + .filter(field => field.mandatory && !columnMappings[field.key]) + .map(field => field.label); + + if (missingMappings.length > 0) { + toast.error(`${t('veuillezMapperLesColonnesSuivantes')} : ${missingMappings.join(', ')}`); + return; + } + + // Préparer les données mappées et parsées + const mappedData = fileData.map(row => { + const mappedRow = {}; + expectedFields.forEach(field => { + const headerIndex = fileHeaders.indexOf(columnMappings[field.key]); + const rawValue = headerIndex !== -1 ? row[headerIndex] : ''; + mappedRow[field.key] = parseValue(rawValue, field.type); + }); + return mappedRow; + }); + + // Envoyer les données au parent ou au backend + onDataMapped(mappedData); + closeMappingModal.current.click(); + }; + + const handleFileChooser = () => { + fileChooser.current.click(); + } + + return
+ + + + + + + + + +
+} diff --git a/src/main/webapp/src/components/ProtectionSelector.jsx b/src/main/webapp/src/components/ProtectionSelector.jsx new file mode 100644 index 0000000..367c5af --- /dev/null +++ b/src/main/webapp/src/components/ProtectionSelector.jsx @@ -0,0 +1,166 @@ +import {useTranslation} from "react-i18next"; + +const ProtectionSelector = ({ + shield = true, + mandatoryProtection = 0, setMandatoryProtection = () => { + } + }) => { + const {t} = useTranslation(); + const toggle = (bit) => { + bit = 1 << (bit - 1); + setMandatoryProtection(v => (v & bit ? v & ~bit : v | bit)); + }; + + const props = { + style: ({cursor: "pointer"}), + }; + + const propsDash = { + style: ({cursor: "pointer"}), + opacity: "0.8", + stroke: "#7b8285", + strokeDasharray: "4 3", + strokeWidth: "1" + }; + + const isOn = (bit) => (mandatoryProtection & (1 << (bit - 1))) !== 0; + const color = (bit) => (isOn(bit) ? "#4ade80" : "#e5e7eb"); + + /* + 1 - 1 - Casque + 2 - 2 - Gorgerin + 3 - 4 - Coquille et Protection pelvienne + 4 - 8 - Gant main(s) armée(s) + 5 - 16 - Gant main bouclier + 6 - 32 - Plastron + 7 - 64 - Protection de bras armé(s) + 8 - 128 - Protection de bras de bouclier + 9 - 256 - Protection de jambes + 10 - 512 - Protection de genoux + 11 - 1024 - Protection de coudes + 12 - 2048 - Protection dorsale + 13 - 4096 - Protection de pieds + */ + return ( + + + + {/* Casque */} + toggle(1)} + >{t('casque')} + + {/* Gorgerin */} + toggle(2)} + >{t('gorgerin')} + + {/* Plastron */} + toggle(6)} + >{t('plastron')} + + {/* Protection dorsale */} + toggle(12)} + >{t('protectionDorsale')} + + {/* Protection de bras armé(s) */} + toggle(7)} + >{shield ? t('protectionDeBrasArmé') : t('protectionDeBras')} + {/* Protection de bras de bouclier */} + toggle(shield ? 8 : 7)} + >{shield ? t('protectionDeBrasDeBouclier') : t('protectionDeBras')} + + {/* Protection de coudes */} + toggle(11)} + >{t('protectionDeCoudes')} + toggle(11)} + >{t('protectionDeCoudes')} + + {/* Gant main(s) armée(s) */} + toggle(4)} + >{shield ? t('gantMainsArmées') : t('gants')} + {/* Gant main bouclier */} + toggle(shield ? 5 : 4)} + >{shield ? t('gantMainBouclier') : t('gants')} + + {/* Protection de jambes */} + toggle(9)} + >{t('protectionDeJambes')} + toggle(9)} + >{t('protectionDeJambes')} + + {/* Protection de genoux */} + toggle(10)} + >{t('protectionDeGenoux')} + toggle(10)} + >{t('protectionDeGenoux')} + + {/* Coquille et Protection pelvienne */} + toggle(3)} + >{t('coquilleProtectionPelvienne')} + + {/* Protection de pieds */} + toggle(13)} + >{t('protectionDePieds')} + toggle(13)} + >{t('protectionDePieds')} + + ); +} + +export default ProtectionSelector; diff --git a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx index a938abd..23fb7db 100644 --- a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx @@ -6,12 +6,24 @@ import {CheckField, OptionField, TextField} from "../../components/MemberCustomF import {ClubSelect} from "../../components/ClubSelect.jsx"; import {ConfirmDialog} from "../../components/ConfirmDialog.jsx"; import {toast} from "react-toastify"; -import {apiAxios, getToastMessage} from "../../utils/Tools.js"; -import {useEffect, useReducer, useState} from "react"; +import { + apiAxios, + CatList, + getCatName, getShieldTypeName, + getSwordTypeName, + getToastMessage, + ShieldList, + sortCategories, + SwordList, + timePrint +} from "../../utils/Tools.js"; +import React, {useEffect, useReducer, useState} from "react"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons"; import {Trans, useTranslation} from "react-i18next"; +import ProtectionSelector from "../../components/ProtectionSelector.jsx"; +import CategoryPreset from "../../assets/CategoryPreset.js"; export function CompetitionEdit() { const {id} = useParams() @@ -44,7 +56,7 @@ export function CompetitionEdit() { {data.id !== null && } {data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") && @@ -190,10 +202,10 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) { }}> - -
-
- +
+
+ +
@@ -205,8 +217,23 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) { function Content({data}) { const navigate = useNavigate(); const [registerMode, setRegisterMode] = useState(data.registerMode || "FREE"); + const [modaleState, setModaleState] = useState({}) + const [presets, setPresets] = useState(data.presets || []); + const [cats, setCats] = useState(data.requiredWeight || []) + const [presetChange, setPresetChange] = useState(false) const {t} = useTranslation(); + const setCat = (e, cat) => { + if (e.target.checked) { + if (!cats.includes(cat)) { + setCats([...cats, cat]) + } + } else { + setCats(cats.filter(c => c !== cat)) + } + } + const isCatSelected = (cat) => cats.includes(cat) + const handleSubmit = (event) => { event.preventDefault(); @@ -227,6 +254,8 @@ function Content({data}) { out['startRegister'] = event.target.startRegister?.value out['endRegister'] = event.target.endRegister?.value out['registerMode'] = registerMode + out['presets'] = presets + out['requiredWeight'] = cats if (out['registerMode'] === "HELLOASSO") { out['data3'] = event.target.data3?.value @@ -277,14 +306,17 @@ function Content({data}) { toast.promise( apiAxios.post(`/competition`, out), getToastMessage("comp.toast.save") ).then(data => { + setPresetChange(false) console.log(data.data) if (data.data.id !== undefined) navigate("/competition/" + data.data.id) + if (data.data.presets !== undefined) + setPresets(data.data.presets) }) } - return
-
+ return <> +
{data.id ? t('comp.editionCompétition') : t('comp.créationCompétition')}
@@ -340,6 +372,56 @@ function Content({data}) {
+
+

+ +

+
+
+ +
+
{presetChange && t('LesModificationsNontEnregistrer')}
+
+
+ + +
    + {CategoryPreset.map((preset, index) => +
  • + +
  • )} +
+
+
+
+
+
+
+

+
+ {t('poidsDemandéPour')} + {CatList.map((cat, index) =>
+ setCat(e, cat)}/> + +
)} +
+
{t('comp.ha.text1')}
@@ -404,14 +496,192 @@ function Content({data}) {
+
+
+ +
+
+
-
-
- + - + +} + +function CatModalContent({setPresets, setPresetChange, state}) { + const [name, setName] = useState(state.name || "") + const [sword, setSword] = useState(state.sword || "NONE") + const [shield, setShield] = useState(state.shield || "NONE") + const [cats, setCats] = useState(state.categories || []) + const [mandatoryProtection1, setMandatoryProtection1] = useState(state.mandatoryProtection1 || 5) + const [mandatoryProtection2, setMandatoryProtection2] = useState(state.mandatoryProtection2 || 5) + + const {t} = useTranslation(); + + useEffect(() => { + setName(state.name || "") + setSword(state.sword || "NONE") + setShield(state.shield || "NONE") + setCats(state.categories?.map(c => ({ + categorie: c.categorie, + roundDuration: timePrint(c.roundDuration), + pauseDuration: timePrint(c.pauseDuration) + })) || []) + setMandatoryProtection1(state.mandatoryProtection1 || 5) + setMandatoryProtection2(state.mandatoryProtection2 || 5) + }, [state]); + + const setCat = (e, cat) => { + if (e.target.checked) { + if (!cats.includes(cat)) { + setCats([...cats, {categorie: cat, roundDuration: "", pauseDuration: ""}]) + } + } else { + setCats(cats.filter(c => c.categorie !== cat)) + } + } + const setTime = (e, cat) => { + const value = e.target.value; + setCats(cats.map(c => { + if (c.categorie === cat) + return {...c, roundDuration: value} + return c + })) + } + const setPause = (e, cat) => { + const value = e.target.value; + setCats(cats.map(c => { + if (c.categorie === cat) + return {...c, pauseDuration: value} + return c + })) + } + + const isCatSelected = (cat) => cats.some(cat_ => cat_.categorie === cat) + + const parseTime = (str) => { + const parts = str.split(":").map(part => parseInt(part, 10)); + if (parts.length === 1) { + return parts[0] * 1000; + } else if (parts.length === 2) { + return (parts[0] * 60 + parts[1]) * 1000; + } else { + return 0; + } + } + + const handleSave = () => { + const out = { + id: state.id, + name: name, + sword: sword, + shield: shield, + categories: cats.map(c => ({ + categorie: c.categorie, + roundDuration: parseTime(c.roundDuration), + pauseDuration: parseTime(c.pauseDuration) + })), + mandatoryProtection1: mandatoryProtection1, + mandatoryProtection2: mandatoryProtection2 + } + setPresets(presets => [...presets.filter(p => p.id !== out.id), out]) + setPresetChange(true) + } + + const handleRm = () => { + setPresets(presets => presets.filter(p => p.id !== state.id)) + setPresetChange(true) + } + + return <> +
+

{t('configurationDeLaCatégorie')}

+ +
+
+
+
+
+ {t("nom")} + setName(e.target.value)}/> +
+ +
+ {t('arme')} + +
+ +
+ {t('bouclier')} + +
+ + + + + + + + + + + + {CatList.map((cat, index) => + + + + + )} + +
{t('catégorie')}{t('peutSinscrire')}{t('duréeRound')}{t('duréePause')}
setCat(e, cat)}/> c.categorie === cat)?.roundDuration || ""} + onChange={e => setTime(e, cat)} + aria-label="mm:ss" hidden={!isCatSelected(cat)} style={{width: "4.5em"}}/> c.categorie === cat)?.pauseDuration || ""} + onChange={e => setPause(e, cat)} + aria-label="mm:ss" hidden={!isCatSelected(cat)} style={{width: "4.5em"}}/>
+
+ +
+
+
{t('protectionObligatoire')} :
+ {cats.some(cat_ => CatList.indexOf(cat_.categorie) <= CatList.indexOf("JUNIOR")) && <> +
< 18 {t('ans')}
+ + } + {cats.some(cat_ => CatList.indexOf(cat_.categorie) > CatList.indexOf("JUNIOR")) && <> +
≥ 18 {t('ans')}
+ + } +
+
+
+
+
+ + + +
+ } diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index 11f17c3..93c8969 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -1,30 +1,36 @@ -import {useNavigate, useParams, useSearchParams} from "react-router-dom"; +import {useNavigate, useParams} from "react-router-dom"; import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx"; import {useFetch} from "../../hooks/useFetch.js"; import {AxiosError} from "../../components/AxiosError.jsx"; import {ThreeDots} from "react-loader-spinner"; -import {useEffect, useReducer, useRef, useState} from "react"; -import {apiAxios, CatList, getCatName, getToastMessage} from "../../utils/Tools.js"; +import React, {useEffect, useId, useReducer, useRef, useState} from "react"; +import {apiAxios, applyOverCategory, CatList, getCatFromName, getCatName, getToastMessage} from "../../utils/Tools.js"; import {toast} from "react-toastify"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faAdd, faGavel, faTrashCan} from "@fortawesome/free-solid-svg-icons"; +import {faAdd, faGavel, faLock, faTrashCan} from "@fortawesome/free-solid-svg-icons"; import "./CompetitionRegisterAdmin.css" import * as XLSX from "xlsx-js-style"; import {useCountries} from "../../hooks/useCountries.jsx"; import {Trans, useTranslation} from "react-i18next"; +import {Checkbox} from "../../components/MemberCustomFiels.jsx"; +import {FileImport} from "../../components/FileImport.jsx"; export function CompetitionRegisterAdmin({source}) { const {id} = useParams() const navigate = useNavigate() const [state, dispatch] = useReducer(SimpleReducer, []) const [clubFilter, setClubFilter] = useState("") - const [catFilter, setCatFilter] = useState("") + const [catAgeFilter, setCatAgeFilter] = useState("") + const [catFilter, setCatFilter] = useState(-1) + const [filterNotWeight, setFilterNotWeight] = useState(false) const [modalState, setModalState] = useState({}) const {t} = useTranslation(); const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/competition/${id}/register/${source}`, setLoading, 1) + const {data: data2, error: error2} = useFetch(`/competition/${id}/categories`, setLoading, 1) + const {data: data3} = useFetch(`/competition/${id}?light=true`, setLoading, 1) const sortName = (a, b) => { if (a.data.fname === b.data.fname) return a.data.lname.localeCompare(b.data.lname); @@ -43,11 +49,31 @@ export function CompetitionRegisterAdmin({source}) { return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), getToastMessage("comp.toast.register.add") ).then((response) => { if (response.data.error) { - return + return null; } dispatch({type: 'UPDATE_OR_ADD', payload: {id: response.data.id, data: response.data}}) dispatch({type: 'SORT', payload: sortName}) - document.getElementById("closeModal").click(); + return response.data + }) + } + const sendRegisters = (new_state) => { + toast.promise(apiAxios.post(`/competition/${id}/registers/${source}`, new_state), getToastMessage("comp.toast.registers.addMultiple") + ).then((response) => { + if (response.data.error) + return; + + let i = 0; + response.data.forEach((d) => { + if (d.licence === -42) { + toast.warn(t('erreurPourLinscription') + " :" + d.lname, {autoClose: false}); + } else { + dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d}}) + i++; + } + }) + if (i > 0) + toast.success(t('comp.toast.register.addMultiple.success', {count: i})) + dispatch({type: 'SORT', payload: sortName}) }) } @@ -62,8 +88,15 @@ export function CompetitionRegisterAdmin({source}) {
{data ?
(clubFilter.length === 0 || s.data.club.name === clubFilter) && (catFilter.length === 0 || s.data.categorie === catFilter))} - dispatch={dispatch} id={id} setModalState={setModalState} source={source}/> + data={state.filter(s => (clubFilter.length === 0 || s.data.club.name === clubFilter) + && (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter) + && (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter)) + && (!filterNotWeight || (data3?.requiredWeight.includes(s.data.categorie) && ( + (source === "admin" && (s.data.weightReal === "" || s.data.weightReal === null)) || + (source !== "admin" && (s.data.weight === "" || s.data.weight === null)) + ))) + )} + data2={data2} data3={data3} dispatch={dispatch} id={id} setModalState={setModalState} source={source}/>
: error ? : }
@@ -77,36 +110,65 @@ export function CompetitionRegisterAdmin({source}) { onClick={() => setModalState({id: -793548328091516928})}>{t('comp.ajouterUnInvité')}
} - +
{t('filtre')}
- +
- {source === "admin" && } + {source === "admin" &&
} + {source === "admin" &&
} + {source === "admin" &&
}
- + } -function QuickAdd({sendRegister, source}) { +function QuickAdd({sendRegister, source, data2, error2}) { const {t} = useTranslation(); + const [categories, setCategories] = useState([]) const handleAdd = (licence) => { - console.log("Quick add licence: " + licence) - sendRegister({ - licence: licence, fname: "", lname: "", weight: "", overCategory: 0, lockEdit: false, id: null + licence: licence, + fname: "", + lname: "", + weight: "", + overCategory: 0, + lockEdit: false, + id: null, + quick: true, + categoriesInscrites: categories }) } + const setCategories_ = (e, catId) => { + if (e.target.checked) { + if (!categories.includes(catId)) { + setCategories([...categories, catId]) + } + } else { + setCategories(categories.filter(c => c !== catId)) + } + } + return
{t('comp.ajoutRapide')}
+
+ + a.name.localeCompare(b.name))} categories={categories} + setCategories={setCategories_}/> +
+
{t('comp.noDeLicence')}
@@ -131,14 +193,14 @@ function QuickAdd({sendRegister, source}) { {source === "club" && - + }
} -function SearchMember({sendRegister}) { +function SearchMember({sendRegister, categories}) { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/club/members`, setLoading, 1) const [suggestions, setSuggestions] = useState([]) @@ -158,7 +220,9 @@ function SearchMember({sendRegister}) { weight: "", overCategory: 0, lockEdit: false, - id: null + id: null, + quick: true, + categoriesInscrites: categories }) } @@ -284,14 +348,41 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => { ); }; -function Modal({sendRegister, modalState, setModalState, source}) { +function CategoriesList({error2, availableCats, fistCatInput, categories, setCategories}) { + const {t} = useTranslation(); + const id = useId(); + + return <> + {error2 ? : <> + {availableCats && availableCats.length === 0 &&
{t('aucuneCatégorieDisponible')}
} + {availableCats && availableCats.map((cat, index) => +
+
+ setCategories(e, cat.id)}/> + +
+
)} + } + +} + +function Modal_({data2, data3, error2, sendRegister, modalState, setModalState, source}) { const country = useCountries('fr') const {t} = useTranslation(); + const closeBtn = useRef(null); + const licenceInput = useRef(null); + const nameInput = useRef(null); + const fistCatInput = useRef(null); + const submitBtn = useRef(null); const [licence, setLicence] = useState("") const [fname, setFname] = useState("") const [lname, setLname] = useState("") const [weight, setWeight] = useState("") + const [weightReal, setWeightReal] = useState("") const [cat, setCat] = useState(0) const [gcat, setGCat] = useState("") const [club, setClub] = useState("") @@ -299,97 +390,127 @@ function Modal({sendRegister, modalState, setModalState, source}) { const [genre, setGenre] = useState("NA") const [editMode, setEditMode] = useState(false) const [lockEdit, setLockEdit] = useState(false) + const [categories, setCategories] = useState([]) useEffect(() => { - console.log(modalState) - if (!modalState) { - setLicence("") - setFname("") - setLname("") - setWeight("") - setCat(0) - setEditMode(false) - setLockEdit(false) - setClub("") - setGCat("") - setCountry_("FR") - setGenre("NA") - } else { - setLicence(modalState.licence ? modalState.licence : "") - setFname(modalState.fname ? modalState.fname : "") - setLname(modalState.lname ? modalState.lname : "") - setWeight(modalState.weight ? modalState.weight : "") - setCat(modalState.overCategory ? modalState.overCategory : 0) - setEditMode(modalState.licence || (modalState.fname && modalState.lname)) - setLockEdit(modalState.lockEdit) - setClub(modalState.club ? modalState.club.name : "") - setGCat(modalState.categorie ? modalState.categorie : "") - setCountry_(modalState.country ? modalState.country : "FR") - setGenre(modalState.genre ? modalState.genre : "NA") - } + setLicence(modalState?.licence ? modalState.licence : "") + setFname(modalState?.fname ? modalState.fname : "") + setLname(modalState?.lname ? modalState.lname : "") + setWeight(modalState?.weight ? modalState.weight : "") + setWeightReal(modalState?.weightReal ? modalState.weightReal : "") + setCat(modalState?.overCategory ? modalState.overCategory : 0) + setEditMode(modalState?.licence || (modalState.fname && modalState.lname)) + setLockEdit(modalState?.lockEdit === undefined ? false : modalState.lockEdit) + setClub(modalState?.club ? modalState.club.name : "") + setGCat(modalState?.categorie ? modalState.categorie : "") + setCountry_(modalState?.country ? modalState.country : "FR") + setGenre(modalState?.genre ? modalState.genre : "NA") + setCategories(modalState?.categoriesInscrites ? modalState.categoriesInscrites : []) + + setTimeout(() => { + if (modalState?.id === 0) { + licenceInput.current?.focus() + } else if (modalState?.id < 0) { + nameInput.current?.focus() + } + }, 450) }, [modalState]); - return