From 4dff0940c1bc7c9a8f204abdedf1e897cf9d4e8c Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sun, 18 Jan 2026 21:10:20 +0100 Subject: [PATCH 1/8] feat: add docker healthcheck for api --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) 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 From a5d3973394f12aa488e9b2d629b0fcd3bf2f7ae5 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 20 Jan 2026 19:05:23 +0100 Subject: [PATCH 2/8] feat: add category registration system to admin --- .../ffsaf/data/model/CatPresetModel.java | 71 ++++ .../ffsaf/data/model/CategoryModel.java | 4 + .../data/model/CompetitionGuestModel.java | 8 + .../ffsaf/data/model/CompetitionModel.java | 2 + .../ffsaf/data/model/RegisterModel.java | 14 + .../data/repository/CatPresetRepository.java | 9 + .../domain/service/CompetPermService.java | 18 +- .../domain/service/CompetitionService.java | 132 ++++++-- .../ffsaf/rest/CompetitionEndpoints.java | 12 +- .../ffsaf/rest/data/CompetitionData.java | 19 +- .../ffsaf/rest/data/PresetData.java | 32 ++ .../ffsaf/rest/data/RegisterRequestData.java | 5 + .../ffsaf/rest/data/SimpleRegisterComb.java | 17 +- src/main/webapp/public/locales/en/cm.json | 8 +- src/main/webapp/public/locales/en/common.json | 22 +- src/main/webapp/public/locales/fr/cm.json | 8 +- src/main/webapp/public/locales/fr/common.json | 24 +- .../src/components/ProtectionSelector.jsx | 99 ++++++ .../src/pages/competition/CompetitionEdit.jsx | 224 +++++++++++- .../competition/CompetitionRegisterAdmin.jsx | 318 ++++++++++++------ .../src/pages/competition/CompetitionView.jsx | 2 +- src/main/webapp/src/utils/Tools.js | 73 ++++ 22 files changed, 941 insertions(+), 180 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java create mode 100644 src/main/java/fr/titionfire/ffsaf/data/repository/CatPresetRepository.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java create mode 100644 src/main/webapp/src/components/ProtectionSelector.jsx 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..950ef4d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java @@ -0,0 +1,71 @@ +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 = ""; + + List categories; + + long roundDuration; + long pauseDuration; + + SwordType swordType = SwordType.NONE; + ShieldType shieldType = ShieldType.NONE; + + /* Bitmask protections: + * 1 - 1 - Head + * 2 - 2 - Throat + * 3 - 4 - Torso + * 4 - 8 - Arms + * 5 - 16 - Hands + * 6 - 32 - Groin + * 7 - 64 - Legs + */ + int mandatoryProtection = 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 + } + +} 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..9c55358 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionGuestModel.java @@ -61,6 +61,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); 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..ceac29e 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -58,6 +58,8 @@ 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 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..c84602d 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 @@ -46,6 +49,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()); 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..d220b14 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,46 @@ 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.setRoundDuration(preset.getRoundDuration()); + presetModel.setPauseDuration(preset.getPauseDuration()); + presetModel.setCategories(preset.getCategories()); + presetModel.setMandatoryProtection(preset.getMandatoryProtection()); + } + + // 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<>()); @@ -271,11 +315,17 @@ 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) + .onItem().call(guest -> Mutiny.fetch(guest.getCategoriesInscrites())) + .map(guest -> SimpleRegisterComb.fromModel(guest) + .setCategorieInscrite(guest.getCategoriesInscrites())) + .collect().asList() .invoke(l::addAll)); }); @@ -290,11 +340,15 @@ 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() + .call(rm -> rm == null ? Uni.createFrom().voidItem() : + Mutiny.fetch(rm.getCategoriesInscrites())) .map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of())))); } @@ -312,7 +366,8 @@ public class CompetitionService { return Panache.withTransaction(() -> repository.persist(c)); }) .chain(combModel -> updateRegister(data, c, combModel, true))) - .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())); + .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 +378,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"); @@ -334,13 +389,21 @@ public class CompetitionService { model.setCountry(data.getCountry()); model.setWeight(data.getWeight()); 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()); }) - .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().contains(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) @@ -418,6 +481,19 @@ public class CompetitionService { } 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().contains(r.getCategorie2())); + }))) .chain(r -> Panache.withTransaction(() -> registerRepository.persist(r))) .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ? sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()); @@ -660,6 +736,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 +775,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, 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() diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java index 77cabc5..c2785af 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; @@ -79,6 +76,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..8b48765 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; @@ -41,6 +43,14 @@ public class CompetitionData { private String data3; private String data4; private String config; + private List presets; + + public CompetitionData () { + this(null, "", "", "", "", new Date(), new Date(), + CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true, + null, "", "", null, true, true, + "", "", "", "", "{}", new ArrayList<>()); + } public static CompetitionData fromModel(CompetitionModel model) { if (model == null) @@ -50,7 +60,7 @@ 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<>()); } public static CompetitionData fromModelLight(CompetitionModel model) { @@ -61,7 +71,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<>()); if (model.getRegisterMode() == RegisterMode.HELLOASSO) { out.setData1(model.getData1()); @@ -84,6 +94,11 @@ public class CompetitionData { 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..1f5dba7 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java @@ -0,0 +1,32 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.CatPresetModel; +import fr.titionfire.ffsaf.utils.Categorie; +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; + long roundDuration; + long pauseDuration; + int mandatoryProtection; + + 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.getRoundDuration(), model.getPauseDuration(), + model.getMandatoryProtection()); + } +} 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..6d72eb0 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 @@ -19,6 +21,7 @@ public class RegisterRequestData { private Integer weight; private int overCategory; private boolean lockEdit = false; + private List categoriesInscrites; // for guest registration only private Long id = null; @@ -26,4 +29,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..c981763 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; @@ -12,6 +9,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; +import java.util.ArrayList; import java.util.List; @Data @@ -30,6 +28,7 @@ public class SimpleRegisterComb { private int overCategory; private boolean hasLicenceActive; private boolean lockEdit; + private List categoriesInscrites; public static SimpleRegisterComb fromModel(RegisterModel register, List licences) { MembreModel membreModel = register.getMembre(); @@ -39,13 +38,19 @@ public class SimpleRegisterComb { SimpleClubModel.fromModel(register.getClub()), membreModel.getLicence(), register.getWeight(), 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(), 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/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..d3dfedf 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", @@ -81,11 +83,14 @@ "ajouterUnClub": "Add a club", "ajouterUnMembre": "Add a member", "all_season": "--- all seasons ---", + "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 +98,6 @@ "button.appliquer": "Apply", "button.confirmer": "Confirm", "button.créer": "Create", - "button.enregister": "Save", "button.enregistrer": "Save", "button.fermer": "Close", "button.modifier": "Edit", @@ -115,6 +119,7 @@ "cat.vétéran2": "Veteran 2", "categorie": "category", "catégorie": "Category", + "catégorieàAjouter": "Category to add", "certificatMédical": "Medical certificate", "chargement...": "Loading...", "chargerLexcel": "Load Excel", @@ -249,6 +254,7 @@ "compte": "Account", "compétition": "Competition", "configuration": "Configuration", + "configurationDeLaCatégorie": "Category configuration", "conserverLancienEmail": "Keep the old email", "contactAdministratif": "Administrative contact", "contactInterne": "Internal contact", @@ -274,6 +280,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", @@ -474,6 +482,7 @@ "permission": "Permission", "photos": "Photos", "prenom": "First name", + "protectionObligatoire": "Mandatory protection", "prénomEtNom": "First and last name", "rechercher": "Search", "rechercher...": "Search...", @@ -494,8 +503,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 +521,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", 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..38c5a56 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", @@ -81,11 +83,14 @@ "ajouterUnClub": "Ajouter un club", "ajouterUnMembre": "Ajouter un membre", "all_season": "--- tout les saisons ---", + "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 +98,6 @@ "button.appliquer": "Appliquer", "button.confirmer": "Confirmer", "button.créer": "Créer", - "button.enregister": "Enregister", "button.enregistrer": "Enregistrer", "button.fermer": "Fermer", "button.modifier": "Modifier", @@ -115,6 +119,7 @@ "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", "chargement...": "Chargement...", "chargerLexcel": "Charger l'Excel", @@ -249,6 +254,7 @@ "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", @@ -274,6 +280,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 ", @@ -474,6 +482,7 @@ "permission": "Permission", "photos": "Photos", "prenom": "Prénom", + "protectionObligatoire": "Protection obligatoire", "prénomEtNom": "Prénom et nom", "rechercher": "Rechercher", "rechercher...": "Rechercher...", @@ -494,8 +503,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 +521,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", diff --git a/src/main/webapp/src/components/ProtectionSelector.jsx b/src/main/webapp/src/components/ProtectionSelector.jsx new file mode 100644 index 0000000..5ee6063 --- /dev/null +++ b/src/main/webapp/src/components/ProtectionSelector.jsx @@ -0,0 +1,99 @@ + +const ProtectionSelector = ({mandatoryProtection = 0, setMandatoryProtection = () => {} }) => { + const toggle = (bit) => { + setMandatoryProtection(v => (v & bit ? v & ~bit : v | bit)); + }; + + const isOn = (bit) => (mandatoryProtection & bit) !== 0; + const color = (bit) => (isOn(bit) ? "#4ade80" : "#e5e7eb"); + + return ( +
+ + + + {/* Head */} + toggle(1)} + /> + + {/* Throat / Neck */} + toggle(2)} + /> + + {/* Torso */} + toggle(4)} + /> + + {/* Arms */} + toggle(8)} + /> + toggle(8)} + /> + + {/* Hands */} + toggle(16)} + /> + toggle(16)} + /> + + {/* Legs */} + toggle(64)} + /> + toggle(64)} + /> + + {/* Groin */} + toggle(32)} + /> + + {/* Feet */} + + + +
+ ); +} + +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..fbadfa5 100644 --- a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx @@ -6,12 +6,23 @@ 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"; export function CompetitionEdit() { const {id} = useParams() @@ -190,10 +201,10 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) { }}> - -
-
- +
+
+ +
@@ -205,6 +216,9 @@ 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 [presetChange, setPresetChange] = useState(false) const {t} = useTranslation(); const handleSubmit = (event) => { @@ -227,6 +241,7 @@ function Content({data}) { out['startRegister'] = event.target.startRegister?.value out['endRegister'] = event.target.endRegister?.value out['registerMode'] = registerMode + out['presets'] = presets if (out['registerMode'] === "HELLOASSO") { out['data3'] = event.target.data3?.value @@ -277,14 +292,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 +358,40 @@ function Content({data}) {
+
+

+ +

+
+
+ +
+
{presetChange && t('LesModificationsNontEnregistrer')}
+
+ +
+
+
+
+
+

+
+
+ +
+
+
-
-
- + - + +} + +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 [time, setTime] = useState(timePrint(state.roundDuration || 90000)) + const [pause, setPause] = useState(timePrint(state.pauseDuration || 60000)) + const [cats, setCats] = useState(state.categories || []) + const [mandatoryProtection, setMandatoryProtection] = useState(state.mandatoryProtection || 33) + + const {t} = useTranslation(); + + useEffect(() => { + setName(state.name || "") + setSword(state.sword || "NONE") + setShield(state.shield || "NONE") + setTime(timePrint(state.roundDuration || 90000)) + setPause(timePrint(state.pauseDuration || 60000)) + setCats(state.categories || []) + setMandatoryProtection(state.mandatoryProtection || 33) + }, [state]); + + 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 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, + roundDuration: parseTime(time), + pauseDuration: parseTime(pause), + categories: cats, + mandatoryProtection: mandatoryProtection + } + 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('duréeRound')} + setTime(e.target.value)}/> + (mm:ss) +
+
+ {t('duréePause')} + setPause(e.target.value)}/> + (mm:ss) +
+ +
+ {t('arme')} + +
+ +
+ {t('bouclier')} + +
+ + +
+ {CatList.map((cat, index) =>
+
+ setCat(e, cat)}/> + +
+
)} +
+
+ +
+
+
{t('protectionObligatoire')} :
+ +
+
+
+
+
+ + + +
+ } diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index 11f17c3..48269b4 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -3,8 +3,8 @@ 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, useReducer, useRef, useState} from "react"; +import {apiAxios, applyOverCategory, CatList, getCatName, getToastMessage} from "../../utils/Tools.js"; import {toast} from "react-toastify"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; @@ -19,12 +19,14 @@ export function CompetitionRegisterAdmin({source}) { 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 [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 sortName = (a, b) => { if (a.data.fname === b.data.fname) return a.data.lname.localeCompare(b.data.lname); @@ -43,11 +45,11 @@ 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 }) } @@ -62,7 +64,9 @@ export function CompetitionRegisterAdmin({source}) {
{data ?
(clubFilter.length === 0 || s.data.club.name === clubFilter) && (catFilter.length === 0 || s.data.categorie === catFilter))} + 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)))} dispatch={dispatch} id={id} setModalState={setModalState} source={source}/>
: error ? : }
@@ -77,36 +81,60 @@ export function CompetitionRegisterAdmin({source}) { onClick={() => setModalState({id: -793548328091516928})}>{t('comp.ajouterUnInvité')}
} - +
{t('filtre')}
- +
{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')}
+
+ + +
+
{t('comp.noDeLicence')}
@@ -131,14 +159,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 +186,9 @@ function SearchMember({sendRegister}) { weight: "", overCategory: 0, lockEdit: false, - id: null + id: null, + quick: true, + categoriesInscrites: categories }) } @@ -284,9 +314,33 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => { ); }; -function Modal({sendRegister, modalState, setModalState, source}) { +function CategoriesList({error2, availableCats, fistCatInput, categories, setCategories}) { + const {t} = useTranslation(); + return <> + {error2 ? : <> + {availableCats && availableCats.length === 0 &&
{t('aucuneCatégorieDisponible')}
} + {availableCats && availableCats.map((cat, index) => +
+
+ setCategories(e, cat.id)}/> + +
+
)} + } + +} + +function Modal({data2, 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("") @@ -299,97 +353,125 @@ 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 : "") + 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 } -function MakeCentralPanel({data, dispatch, id, setModalState, source}) { +function MakeCentralPanel({data, data2, dispatch, id, setModalState, source}) { const [searchParams] = useSearchParams(); const registerType = searchParams.get("type") || "FREE"; const {t} = useTranslation(); @@ -610,14 +610,22 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
{req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"}
-
{req.data.fname} {req.data.lname} {req.data.genre}
+
{req.data.fname} {req.data.lname} {req.data.lockEdit && + }{req.data.genre}
{req.data.club?.name || t("club", {count: 0})}
- {t("comp.surclassement", {count: req.data.overCategory, cat: getCatName(req.data.categorie)})}
- {req.data.weight ? req.data.weight : "---"} kg + {t("comp.surclassement", { + count: req.data.overCategory, + cat: getCatName(req.data.categorie) + })} | {req.data.weight ? req.data.weight : "---"} kg +
+ + {req.data.categoriesInscrites.map(catId => data2?.find(c => c.id === catId)).filter(o => o !== undefined) + .sort((a, b) => a.name.localeCompare(b.name)).map(cat => + {cat.name})}
diff --git a/src/main/webapp/src/pages/competition/CompetitionView.jsx b/src/main/webapp/src/pages/competition/CompetitionView.jsx index a9cebe6..c17b816 100644 --- a/src/main/webapp/src/pages/competition/CompetitionView.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionView.jsx @@ -74,8 +74,8 @@ function MakeContent({data}) { disabled={new Date() < new Date(data.startRegister.split('+')[0]) || new Date() > new Date(data.endRegister.split('+')[0])} onClick={_ => navigate("/competition/" + data.id + "/club/register")}>{t('comp.inscription')} } - {data.registerMode === "FREE" && !isClubAdmin(userinfo) && - + {data.registerMode === "FREE" && !isClubAdmin(userinfo) && + || } {data.registerMode === "HELLOASSO" &&

{t('comp.billetterie')} : } +function ShowRegister() { + const {id} = useParams() + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/competition/${id}/register/user`, setLoading, 1) + const {data: data3, error: error2} = useFetch(`/competition/${id}/categories`, setLoading, 1) + const {t} = useTranslation(); + + + return <> + {data ? data.length > 0 + ?

: {t('vousNêtesPasEncoreInscrit')} + : error + ? + : + } + +} + function Def() { return
  • From d2a7e6cbac32519d95b83ffaf20f54ae6b040646 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 21 Jan 2026 22:05:58 +0100 Subject: [PATCH 5/8] feat: rework CatPreset for add more detail + add preset --- .../ffsaf/data/model/CatPresetModel.java | 44 +++- .../domain/service/CompetitionService.java | 11 +- .../ffsaf/domain/service/ResultService.java | 2 +- .../ffsaf/rest/data/PresetData.java | 11 +- src/main/webapp/public/locales/en/common.json | 17 ++ src/main/webapp/public/locales/fr/common.json | 17 ++ src/main/webapp/src/assets/CategoryPreset.js | 115 +++++++++ .../src/components/ProtectionSelector.jsx | 225 ++++++++++++------ .../src/pages/competition/CompetitionEdit.jsx | 139 +++++++---- .../competition/CompetitionRegisterAdmin.jsx | 13 +- .../src/pages/competition/CompetitionView.jsx | 4 +- 11 files changed, 440 insertions(+), 158 deletions(-) create mode 100644 src/main/webapp/src/assets/CategoryPreset.js diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java index 950ef4d..56e3c65 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CatPresetModel.java @@ -28,24 +28,30 @@ public class CatPresetModel { String name = ""; - List categories; - - long roundDuration; - long pauseDuration; + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "category_preset_catconfig", joinColumns = @JoinColumn(name = "id_preset")) + List categories; SwordType swordType = SwordType.NONE; ShieldType shieldType = ShieldType.NONE; - /* Bitmask protections: - * 1 - 1 - Head - * 2 - 2 - Throat - * 3 - 4 - Torso - * 4 - 8 - Arms - * 5 - 16 - Hands - * 6 - 32 - Groin - * 7 - 64 - Legs + /* + * 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 mandatoryProtection = 0; + int mandatoryProtection1 = 0; + int mandatoryProtection2 = 0; @ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY) private List registers = new ArrayList<>(); @@ -68,4 +74,16 @@ public class CatPresetModel { 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/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index b714a5b..6ac79b7 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -271,10 +271,9 @@ public class CompetitionService { presetModel.setName(preset.getName()); presetModel.setSwordType(preset.getSword()); presetModel.setShieldType(preset.getShield()); - presetModel.setRoundDuration(preset.getRoundDuration()); - presetModel.setPauseDuration(preset.getPauseDuration()); presetModel.setCategories(preset.getCategories()); - presetModel.setMandatoryProtection(preset.getMandatoryProtection()); + presetModel.setMandatoryProtection1(preset.getMandatoryProtection1()); + presetModel.setMandatoryProtection2(preset.getMandatoryProtection2()); } // Remove deleted presets @@ -399,7 +398,8 @@ public class CompetitionService { g.getCategoriesInscrites().clear(); g.getCategoriesInscrites().addAll(cats); g.getCategoriesInscrites() - .removeIf(cat -> !cat.getCategories().contains(g.getCategorie())); + .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 ? @@ -495,7 +495,8 @@ public class CompetitionService { } r.getCategoriesInscrites().addAll(cats); r.getCategoriesInscrites() - .removeIf(cat -> !cat.getCategories().contains(r.getCategorie2())); + .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 ? 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/data/PresetData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java index 1f5dba7..60ec743 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java @@ -1,7 +1,6 @@ package fr.titionfire.ffsaf.rest.data; import fr.titionfire.ffsaf.data.model.CatPresetModel; -import fr.titionfire.ffsaf.utils.Categorie; import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.AllArgsConstructor; import lombok.Data; @@ -16,17 +15,15 @@ public class PresetData { private String name; private CatPresetModel.SwordType sword; private CatPresetModel.ShieldType shield; - private List categories; - long roundDuration; - long pauseDuration; - int mandatoryProtection; + 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.getRoundDuration(), model.getPauseDuration(), - model.getMandatoryProtection()); + model.getCategories(), model.getMandatoryProtection1(), model.getMandatoryProtection2()); } } diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json index eed61fa..5f0747c 100644 --- a/src/main/webapp/public/locales/en/common.json +++ b/src/main/webapp/public/locales/en/common.json @@ -83,6 +83,7 @@ "ajouterUnClub": "Add a club", "ajouterUnMembre": "Add a member", "all_season": "--- all seasons ---", + "ans": "years", "arme": "Weapon", "au": "to", "aucun": "None", @@ -105,6 +106,7 @@ "button.seDésinscrire": "Unsubscribe", "button.suivant": "Next", "button.supprimer": "Delete", + "casque": "Helmet", "cat.benjamin": "Benjamin", "cat.cadet": "Cadet", "cat.catégorieInconnue": "Unknown category", @@ -260,6 +262,7 @@ "contactInterne": "Internal contact", "contact_one": "Contact", "contact_other": "Contacts", + "coquilleProtectionPelvienne": "Shell / Pelvic protection", "date": "Date", "dateDeNaissance": "Date of birth", "days": [ @@ -299,8 +302,12 @@ "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": { @@ -480,8 +487,18 @@ "perm.créerDesCompétion": "Create competitions", "perm.ffsafIntra": "FFSAF intra", "permission": "Permission", + "peutSinscrire": "Can register?", "photos": "Photos", + "plastron": "Breastplate", "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", diff --git a/src/main/webapp/public/locales/fr/common.json b/src/main/webapp/public/locales/fr/common.json index 29aaa1e..68f6a30 100644 --- a/src/main/webapp/public/locales/fr/common.json +++ b/src/main/webapp/public/locales/fr/common.json @@ -83,6 +83,7 @@ "ajouterUnClub": "Ajouter un club", "ajouterUnMembre": "Ajouter un membre", "all_season": "--- tout les saisons ---", + "ans": "ans", "arme": "Arme", "au": "au", "aucun": "Aucun", @@ -105,6 +106,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", @@ -260,6 +262,7 @@ "contactInterne": "Contact interne", "contact_one": "Contact", "contact_other": "Contacts", + "coquilleProtectionPelvienne": "Coquille / Protection pelvienne", "date": "Date", "dateDeNaissance": "Date de naissance", "days": [ @@ -299,8 +302,12 @@ "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": { @@ -480,8 +487,18 @@ "perm.créerDesCompétion": "Créer des compétion", "perm.ffsafIntra": "FFSAF intra", "permission": "Permission", + "peutSinscrire": "Peut s'inscrire?", "photos": "Photos", + "plastron": "Plastron", "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", 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/ProtectionSelector.jsx b/src/main/webapp/src/components/ProtectionSelector.jsx index 5ee6063..367c5af 100644 --- a/src/main/webapp/src/components/ProtectionSelector.jsx +++ b/src/main/webapp/src/components/ProtectionSelector.jsx @@ -1,98 +1,165 @@ +import {useTranslation} from "react-i18next"; -const ProtectionSelector = ({mandatoryProtection = 0, setMandatoryProtection = () => {} }) => { +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 isOn = (bit) => (mandatoryProtection & bit) !== 0; + 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 ( -
    - - + + - {/* Head */} - toggle(1)} - /> + {/* Casque */} + toggle(1)} + >{t('casque')} - {/* Throat / Neck */} - toggle(2)} - /> + {/* Gorgerin */} + toggle(2)} + >{t('gorgerin')} - {/* Torso */} - toggle(4)} - /> + {/* Plastron */} + toggle(6)} + >{t('plastron')} - {/* Arms */} - toggle(8)} - /> - toggle(8)} - /> + {/* Protection dorsale */} + toggle(12)} + >{t('protectionDorsale')} - {/* Hands */} - toggle(16)} - /> - toggle(16)} - /> + {/* 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')} - {/* Legs */} - toggle(64)} - /> - toggle(64)} - /> + {/* Protection de coudes */} + toggle(11)} + >{t('protectionDeCoudes')} + toggle(11)} + >{t('protectionDeCoudes')} - {/* Groin */} - toggle(32)} - /> + {/* 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')} - {/* Feet */} - - - -
    + {/* 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')} + ); } diff --git a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx index fbadfa5..064dcf8 100644 --- a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx @@ -23,6 +23,7 @@ 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() @@ -372,7 +373,7 @@ function Content({data}) {
    setModaleState(preset)}> {preset.name} - {preset.categories.sort(sortCategories).map((cat, index) => + {preset.categories.map(e => e.categorie).sort(sortCategories).map((cat, index) => {getCatName(cat)})} )} @@ -381,11 +382,27 @@ function Content({data}) {
    {presetChange && t('LesModificationsNontEnregistrer')}
    - +
    + + +
      + {CategoryPreset.map((preset, index) => +
    • + +
    • )} +
    +
    @@ -479,10 +496,9 @@ 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 [time, setTime] = useState(timePrint(state.roundDuration || 90000)) - const [pause, setPause] = useState(timePrint(state.pauseDuration || 60000)) const [cats, setCats] = useState(state.categories || []) - const [mandatoryProtection, setMandatoryProtection] = useState(state.mandatoryProtection || 33) + const [mandatoryProtection1, setMandatoryProtection1] = useState(state.mandatoryProtection1 || 5) + const [mandatoryProtection2, setMandatoryProtection2] = useState(state.mandatoryProtection2 || 5) const {t} = useTranslation(); @@ -490,23 +506,42 @@ function CatModalContent({setPresets, setPresetChange, state}) { setName(state.name || "") setSword(state.sword || "NONE") setShield(state.shield || "NONE") - setTime(timePrint(state.roundDuration || 90000)) - setPause(timePrint(state.pauseDuration || 60000)) - setCats(state.categories || []) - setMandatoryProtection(state.mandatoryProtection || 33) + 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, cat]) + setCats([...cats, {categorie: cat, roundDuration: "", pauseDuration: ""}]) } } else { - setCats(cats.filter(c => c !== cat)) + 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.includes(cat) + const isCatSelected = (cat) => cats.some(cat_ => cat_.categorie === cat) const parseTime = (str) => { const parts = str.split(":").map(part => parseInt(part, 10)); @@ -525,10 +560,13 @@ function CatModalContent({setPresets, setPresetChange, state}) { name: name, sword: sword, shield: shield, - roundDuration: parseTime(time), - pauseDuration: parseTime(pause), - categories: cats, - mandatoryProtection: mandatoryProtection + 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) @@ -553,19 +591,6 @@ function CatModalContent({setPresets, setPresetChange, state}) { value={name} onChange={e => setName(e.target.value)}/> -
    - {t('duréeRound')} - setTime(e.target.value)}/> - (mm:ss) -
    -
    - {t('duréePause')} - setPause(e.target.value)}/> - (mm:ss) -
    -
    {t('arme')} setCat(e, cat)}/> - -
    - )} - + + + + + + + + + + + {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 0d88ca1..04638b7 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -3,7 +3,7 @@ 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 React, {useEffect, useReducer, useRef, useState} from "react"; +import React, {useEffect, useId, useReducer, useRef, useState} from "react"; import {apiAxios, applyOverCategory, CatList, getCatName, getToastMessage} from "../../utils/Tools.js"; import {toast} from "react-toastify"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; @@ -317,6 +317,8 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => { function CategoriesList({error2, availableCats, fistCatInput, categories, setCategories}) { const {t} = useTranslation(); + const id = useId(); + return <> {error2 ? : <> {availableCats && availableCats.length === 0 &&
    {t('aucuneCatégorieDisponible')}
    } @@ -325,9 +327,9 @@ function CategoriesList({error2, availableCats, fistCatInput, categories, setCat style={{display: "contents"}}>
    setCategories(e, cat.id)}/> - +
    )} } @@ -427,7 +429,7 @@ function Modal({data2, error2, sendRegister, modalState, setModalState, source}) } const currenCat = gcat !== "" ? applyOverCategory(gcat, cat) : ""; - const availableCats = data2 ? (currenCat !== "" ? data2.filter(c => c.categories.includes(currenCat)) : data2).sort((a, b) => a.name.localeCompare(b.name)) : [] + const availableCats = data2 ? (currenCat !== "" ? data2.filter(c => c.categories.some(c2 => c2.categorie === currenCat)) : data2).sort((a, b) => a.name.localeCompare(b.name)) : [] if (availableCats.length === 0) { if (fistCatInput.current) { fistCatInput.current = null @@ -625,7 +627,8 @@ function MakeCentralPanel({data, data2, dispatch, id, setModalState, source}) { {req.data.categoriesInscrites.map(catId => data2?.find(c => c.id === catId)).filter(o => o !== undefined) .sort((a, b) => a.name.localeCompare(b.name)).map(cat => - {cat.name})} + {cat.name})} diff --git a/src/main/webapp/src/pages/competition/CompetitionView.jsx b/src/main/webapp/src/pages/competition/CompetitionView.jsx index c17b816..9aec3d5 100644 --- a/src/main/webapp/src/pages/competition/CompetitionView.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionView.jsx @@ -74,7 +74,7 @@ function MakeContent({data}) { disabled={new Date() < new Date(data.startRegister.split('+')[0]) || new Date() > new Date(data.endRegister.split('+')[0])} onClick={_ => navigate("/competition/" + data.id + "/club/register")}>{t('comp.inscription')} } - {data.registerMode === "FREE" && !isClubAdmin(userinfo) && + {data.registerMode === "FREE" && !isClubAdmin(userinfo) && || } {data.registerMode === "HELLOASSO" && @@ -147,7 +147,7 @@ function SelfRegister({data2}) { } const currenCat = data?.length > 0 && data[0]?.categorie !== "" ? applyOverCategory(data[0]?.categorie, cat) : ""; - const availableCats = data3 ? (currenCat !== "" ? data3.filter(c => c.categories.includes(currenCat)) : data3).sort((a, b) => a.name.localeCompare(b.name)) : [] + const availableCats = data3 ? (currenCat !== "" ? data3.filter(c => c.categories.some(c2 => c2.categorie === currenCat)) : data3).sort((a, b) => a.name.localeCompare(b.name)) : [] return <> {data From 4a07eb4ed92041955062891323ff684219b5c196 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 22 Jan 2026 15:13:48 +0100 Subject: [PATCH 6/8] feat: comp register optional weight config --- .../data/model/CompetitionGuestModel.java | 5 ++ .../ffsaf/data/model/CompetitionModel.java | 3 + .../ffsaf/data/model/RegisterModel.java | 7 ++ .../domain/service/CompetitionService.java | 21 ++++-- .../ffsaf/rest/data/CompetitionData.java | 16 +++-- .../ffsaf/rest/data/RegisterRequestData.java | 1 + .../ffsaf/rest/data/SimpleRegisterComb.java | 5 +- src/main/webapp/public/locales/en/common.json | 4 ++ src/main/webapp/public/locales/fr/common.json | 4 ++ .../src/pages/competition/CompetitionEdit.jsx | 25 ++++++- .../competition/CompetitionRegisterAdmin.jsx | 66 +++++++++++++++---- .../src/pages/competition/CompetitionView.jsx | 20 ++++-- 12 files changed, 142 insertions(+), 35 deletions(-) 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 9c55358..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( @@ -107,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 ceac29e..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; @@ -61,6 +62,8 @@ public class CompetitionModel { @OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) List catPreset = new ArrayList<>(); + List requiredWeight = new ArrayList<>(); + List banMembre = new ArrayList<>(); String owner; 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 c84602d..75c03bd 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -38,6 +38,7 @@ public class RegisterModel { MembreModel membre; Integer weight; + Integer weightReal; int overCategory = 0; Categorie categorie; @@ -89,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/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index 6ac79b7..7282864 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -301,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()); @@ -388,7 +389,9 @@ public class CompetitionService { model.setGenre(data.getGenre()); model.setClub(data.getClub()); model.setCountry(data.getCountry()); - model.setWeight(data.getWeight()); + model.setWeightReal(data.getWeightReal()); + if (model.getCompetition().getRequiredWeight().contains(model.getCategorie())) + model.setWeight(data.getWeight()); model.setCategorie(data.getCategorie()); }) .call(g -> Mutiny.fetch(g.getCategoriesInscrites())) @@ -452,7 +455,6 @@ public class CompetitionService { if (r != null) { if (!admin && r.isLockEdit()) throw new DForbiddenException(trad.t("insc.err3")); - r.setWeight(data.getWeight()); r.setOverCategory(data.getOverCategory()); r.setCategorie( (combModel.getBirth_date() == null) ? combModel.getCategorie() : @@ -461,24 +463,29 @@ public class CompetitionService { int days = Utils.getDaysBeforeCompetition(c.getDate()); if (days > -7) r.setClub(combModel.getClub()); - if (admin) + if (c.getRequiredWeight().contains(r.getCategorie2())) + 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()); } @@ -779,7 +786,7 @@ public class CompetitionService { public Uni registerHelloAsso(NotificationData data) { String organizationSlug = data.getOrganizationSlug(); String formSlug = data.getFormSlug(); - RegisterRequestData req = new RegisterRequestData(null, "", "", null, 0, false, new ArrayList<>(), null, + 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() 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 8b48765..df37ffe 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/CompetitionData.java @@ -35,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; @@ -44,12 +44,13 @@ public class CompetitionData { private String data4; private String config; private List presets; + private List requiredWeight; - public CompetitionData () { + 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<>(), new ArrayList<>()); } public static CompetitionData fromModel(CompetitionModel model) { @@ -60,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(), new ArrayList<>()); + model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig(), + new ArrayList<>(), model.getRequiredWeight()); } public static CompetitionData fromModelLight(CompetitionModel model) { @@ -71,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<>()); + "", "", "", "", "{}", new ArrayList<>(), model.getRequiredWeight()); if (model.getRegisterMode() == RegisterMode.HELLOASSO) { out.setData1(model.getData1()); @@ -85,11 +87,11 @@ 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; } 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 6d72eb0..0665be5 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -19,6 +19,7 @@ public class RegisterRequestData { private String lname; private Integer weight; + private Integer weightReal; private int overCategory; private boolean lockEdit = false; private List categoriesInscrites; 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 c981763..b4b3b21 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java @@ -25,6 +25,7 @@ public class SimpleRegisterComb { private SimpleClubModel club; private Integer licence; private Integer weight; + private Integer weightReal; private int overCategory; private boolean hasLicenceActive; private boolean lockEdit; @@ -36,7 +37,7 @@ 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(), new ArrayList<>()); } @@ -45,7 +46,7 @@ public class SimpleRegisterComb { 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<>()); } diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json index 5f0747c..7e137bd 100644 --- a/src/main/webapp/public/locales/en/common.json +++ b/src/main/webapp/public/locales/en/common.json @@ -76,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}}", @@ -193,7 +194,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", @@ -490,6 +493,7 @@ "peutSinscrire": "Can register?", "photos": "Photos", "plastron": "Breastplate", + "poidsDemandéPour": "Weight required for", "prenom": "First name", "protectionDeBras": "Arm protection", "protectionDeBrasArmé": "Protection of armed arm(s)", diff --git a/src/main/webapp/public/locales/fr/common.json b/src/main/webapp/public/locales/fr/common.json index 68f6a30..934cabf 100644 --- a/src/main/webapp/public/locales/fr/common.json +++ b/src/main/webapp/public/locales/fr/common.json @@ -76,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}}", @@ -193,7 +194,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", @@ -490,6 +493,7 @@ "peutSinscrire": "Peut s'inscrire?", "photos": "Photos", "plastron": "Plastron", + "poidsDemandéPour": "Poids demandé pour", "prenom": "Prénom", "protectionDeBras": "Protection de bras", "protectionDeBrasArmé": "Protection de bras armé(s)", diff --git a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx index 064dcf8..23fb7db 100644 --- a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx @@ -56,7 +56,7 @@ export function CompetitionEdit() { {data.id !== null && } {data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") && @@ -219,9 +219,21 @@ function Content({data}) { 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(); @@ -243,6 +255,7 @@ function Content({data}) { 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 @@ -444,6 +457,16 @@ function Content({data}) { defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/> +
    + {t('poidsDemandéPour')} + {CatList.map((cat, index) =>
    + setCat(e, cat)}/> + +
    )} +
    +
    {t('comp.ha.text1')}
    diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index 04638b7..c729efa 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -1,4 +1,4 @@ -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"; @@ -13,6 +13,7 @@ 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"; export function CompetitionRegisterAdmin({source}) { const {id} = useParams() @@ -21,12 +22,14 @@ export function CompetitionRegisterAdmin({source}) { const [clubFilter, setClubFilter] = 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); @@ -66,8 +69,13 @@ export function CompetitionRegisterAdmin({source}) { (clubFilter.length === 0 || s.data.club.name === clubFilter) && (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter) - && (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter)))} - data2={data2} dispatch={dispatch} id={id} setModalState={setModalState} source={source}/> + && (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 ? : }
    @@ -86,14 +94,16 @@ export function CompetitionRegisterAdmin({source}) {
    {t('filtre')}
    + setCatFilter={setCatFilter} catAgeFilter={catAgeFilter} setCatAgeFilter={setCatAgeFilter} + filterNotWeight={filterNotWeight} setFilterNotWeight={setFilterNotWeight} source={source}/>
    {source === "admin" && } - + } @@ -336,7 +346,7 @@ function CategoriesList({error2, availableCats, fistCatInput, categories, setCat } -function Modal({data2, error2, sendRegister, modalState, setModalState, source}) { +function Modal({data2, data3, error2, sendRegister, modalState, setModalState, source}) { const country = useCountries('fr') const {t} = useTranslation(); const closeBtn = useRef(null); @@ -349,6 +359,7 @@ function Modal({data2, error2, sendRegister, modalState, setModalState, source}) 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("") @@ -363,6 +374,7 @@ function Modal({data2, error2, sendRegister, modalState, setModalState, source}) 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) @@ -398,6 +410,7 @@ function Modal({data2, error2, sendRegister, modalState, setModalState, source}) fname: fname.trim(), lname: lname.trim(), weight: weight, + weightReal: weightReal, overCategory: cat, lockEdit: lockEdit, categoriesInscrites: categories, @@ -517,8 +530,14 @@ function Modal({data2, error2, sendRegister, modalState, setModalState, source})
    {t('comp.modal.poids')} - setWeight(e.target.value)}/> + {source === "admin" && {t('comp.modal.annoncé')}} + setWeight(e.target.value)}/> + {source === "admin" && <>{t('comp.modal.pesé')} + setWeightReal(e.target.value)}/>}
    - {source === "admin" && } + {source === "admin" && } @@ -721,29 +721,78 @@ function MakeCentralPanel({data, data2, data3, dispatch, id, setModalState, sour } -function FileOutput({data}) { +function FileOutput({data, data2}) { const {t} = useTranslation(); + const handleFileDownload = () => { + const catColumns = {} + for (const cat of data2) { + catColumns[cat.id] = "" + } + + const columnOrder = [ + "licence", "pays", "nom", "prenom", "genre", "weight", + "categorie", "overCategory", "categorie2", "club", + ...Object.keys(catColumns) + ]; + const dataOut = [] for (const e of data) { const tmp = { licence: e.licence, + pays: e.country, nom: e.lname, prenom: e.fname, genre: e.genre, - weight: e.weight, - categorie: e.categorie, + weight: e.weightReal ? e.weightReal : e.weight, + categorie: getCatName(e.categorie), overCategory: e.overCategory, + categorie2: getCatName(applyOverCategory(e.categorie, e.overCategory)), club: e.club ? e.club.name : '', + ...catColumns + } + for (const c of e.categoriesInscrites) { + tmp[c] = "X" } dataOut.push(tmp) } + dataOut.sort((a, b) => a.prenom.localeCompare(b.prenom) || a.nom.localeCompare(b.nom)); + + const secondHeaders = [ + "Licence", "Pays", "Nom", "Prénom", "Genre", "Poids", + "Catégorie normalizer", "Surclassement", "Catégorie d'inscription", "Club", + ...Object.keys(catColumns).map(id => data2.find(p => p.id === Number(id))?.name) + ]; + const headers = [ + "", "", "", "", "", "", "", "", "", "", "Catégories", + ...Object.keys(catColumns).map(() => "") + ]; + + const orderedData = dataOut.map(row => columnOrder.map(col => row[col])); const wb = XLSX.utils.book_new(); - const ws = XLSX.utils.json_to_sheet(dataOut); - XLSX.utils.sheet_add_aoa(ws, [["Licence", "Nom", "Prénom", "Genre", "Poids", "Catégorie normalizer", "Surclassement", "Club"]], {origin: 'A1'}); + const ws = XLSX.utils.json_to_sheet([], {skipHeader: true}); - ws["!cols"] = [{wch: 7}, {wch: 16}, {wch: 16}, {wch: 6}, {wch: 6}, {wch: 10}, {wch: 10}, {wch: 60}] + XLSX.utils.sheet_add_aoa(ws, [headers, secondHeaders, ...orderedData], {origin: "A1"}); + + // Fusionner les cellules pour le titre "Catégories" + const mergeStart = XLSX.utils.encode_cell({r: 0, c: 10}); // Ligne 1, colonne K (index 10) + const mergeEnd = XLSX.utils.encode_cell({r: 0, c: 10 + Object.keys(catColumns).length - 1}); + ws["!merges"] = [{s: mergeStart, e: mergeEnd}]; + + // 10. Appliquer une rotation de 45° aux en-têtes + const headerRow = ws["!rows"] || (ws["!rows"] = {}); + headerRow[1] = {hpt: 70}; // Hauteur de la première ligne + for (let i = 0; i < headers.length; i++) { + const cellRef = XLSX.utils.encode_cell({r: 1, c: i}); + if (!ws[cellRef]) ws[cellRef] = {}; + ws[cellRef].s = { + alignment: {textRotation: 45, vertical: "bottom", wrapText: true} + }; + } + + ws["!cols"] = [{wch: 5}, {wch: 4}, {wch: 16}, {wch: 16}, {wch: 4}, {wch: 4}, {wch: 10}, {wch: 4}, {wch: 10}, {wch: 60}, + ...Object.keys(catColumns).map(() => ({wch: 2}))] XLSX.utils.book_append_sheet(wb, ws, "Feuille 1"); XLSX.writeFile(wb, "output.xlsx"); From 197ee0d5b19495a449dd606001898005525deaec Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 22 Jan 2026 22:01:03 +0100 Subject: [PATCH 8/8] feat: register import --- .../domain/service/CompetitionService.java | 120 ++++++++- .../ffsaf/rest/CompetitionEndpoints.java | 10 + .../ffsaf/rest/data/RegisterRequestData.java | 2 +- .../ffsaf/rest/data/SimpleRegisterComb.java | 2 + src/main/webapp/public/locales/en/common.json | 81 +++++- src/main/webapp/public/locales/fr/common.json | 85 +++++- src/main/webapp/src/components/FileImport.jsx | 241 ++++++++++++++++++ .../competition/CompetitionRegisterAdmin.jsx | 142 ++++++++++- src/main/webapp/src/utils/Tools.js | 29 +++ 9 files changed, 695 insertions(+), 17 deletions(-) create mode 100644 src/main/webapp/src/components/FileImport.jsx 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 7282864..110482e 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -367,7 +367,7 @@ public class CompetitionService { c.getBanMembre().remove(combModel.getId()); return Panache.withTransaction(() -> repository.persist(c)); }) - .chain(combModel -> updateRegister(data, c, combModel, true))) + .chain(combModel -> updateRegister(data, c, combModel, true, false))) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) .setCategorieInscrite(r.getCategoriesInscrites())); } else { @@ -390,9 +390,9 @@ public class CompetitionService { model.setClub(data.getClub()); model.setCountry(data.getCountry()); model.setWeightReal(data.getWeightReal()); + model.setCategorie(data.getCategorie()); if (model.getCompetition().getRequiredWeight().contains(model.getCategorie())) model.setWeight(data.getWeight()); - model.setCategorie(data.getCategorie()); }) .call(g -> Mutiny.fetch(g.getCategoriesInscrites())) .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(), @@ -427,7 +427,7 @@ public class CompetitionService { if (c.getBanMembre().contains(model.getId())) throw new DForbiddenException(trad.t("insc.err1")); })) - .chain(combModel -> updateRegister(data, c, combModel, false))) + .chain(combModel -> updateRegister(data, c, combModel, false, false))) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()) .setCategorieInscrite(r.getCategoriesInscrites())); @@ -443,19 +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))) + .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.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(), @@ -464,7 +546,8 @@ public class CompetitionService { if (days > -7) r.setClub(combModel.getClub()); if (c.getRequiredWeight().contains(r.getCategorie2())) - r.setWeight(data.getWeight()); + if (data.getCountry() != null || !append) + r.setWeight(data.getWeight()); if (admin) { r.setWeightReal(data.getWeightReal()); r.setLockEdit(data.isLockEdit()); @@ -510,6 +593,27 @@ public class CompetitionService { 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() @@ -821,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/rest/CompetitionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java index c2785af..5a21ab2 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java @@ -50,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 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 0665be5..93d20e5 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -20,7 +20,7 @@ public class RegisterRequestData { private Integer weight; private Integer weightReal; - private int overCategory; + private Integer overCategory; private boolean lockEdit = false; private List categoriesInscrites; 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 b4b3b21..0dd5799 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java @@ -8,12 +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; diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json index 7e137bd..f7f7f12 100644 --- a/src/main/webapp/public/locales/en/common.json +++ b/src/main/webapp/public/locales/en/common.json @@ -124,6 +124,7 @@ "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.", @@ -154,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", @@ -231,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", @@ -300,6 +304,7 @@ "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", @@ -322,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.", @@ -330,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": { @@ -458,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": { @@ -493,6 +505,7 @@ "peutSinscrire": "Can register?", "photos": "Photos", "plastron": "Breastplate", + "poids": "Weight", "poidsDemandéPour": "Weight required for", "prenom": "First name", "protectionDeBras": "Arm protection", @@ -577,9 +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/common.json b/src/main/webapp/public/locales/fr/common.json index 5355b57..8a11036 100644 --- a/src/main/webapp/public/locales/fr/common.json +++ b/src/main/webapp/public/locales/fr/common.json @@ -124,6 +124,7 @@ "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.", @@ -154,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", @@ -231,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", @@ -300,6 +304,7 @@ "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", @@ -322,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.", @@ -330,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": { @@ -458,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": { @@ -493,6 +505,7 @@ "peutSinscrire": "Peut s'inscrire?", "photos": "Photos", "plastron": "Plastron", + "poids": "Poids", "poidsDemandéPour": "Poids demandé pour", "prenom": "Prénom", "protectionDeBras": "Protection de bras", @@ -577,9 +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/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/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index 5a37561..93c8969 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -4,7 +4,7 @@ import {useFetch} from "../../hooks/useFetch.js"; import {AxiosError} from "../../components/AxiosError.jsx"; import {ThreeDots} from "react-loader-spinner"; import React, {useEffect, useId, useReducer, useRef, useState} from "react"; -import {apiAxios, applyOverCategory, CatList, getCatName, getToastMessage} from "../../utils/Tools.js"; +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"; @@ -14,6 +14,7 @@ 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() @@ -55,6 +56,26 @@ export function CompetitionRegisterAdmin({source}) { 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}) + }) + } return

    {t('comp.combattantsInscrits')}

    @@ -98,12 +119,14 @@ export function CompetitionRegisterAdmin({source}) { filterNotWeight={filterNotWeight} setFilterNotWeight={setFilterNotWeight} source={source}/>
    - {source === "admin" && } + {source === "admin" &&
    } + {source === "admin" &&
    } + {source === "admin" &&
    } - + } @@ -346,7 +369,7 @@ function CategoriesList({error2, availableCats, fistCatInput, categories, setCat } -function Modal({data2, data3, error2, sendRegister, modalState, setModalState, source}) { +function Modal_({data2, data3, error2, sendRegister, modalState, setModalState, source}) { const country = useCountries('fr') const {t} = useTranslation(); const closeBtn = useRef(null); @@ -739,7 +762,7 @@ function FileOutput({data, data2}) { const dataOut = [] for (const e of data) { const tmp = { - licence: e.licence, + licence: e.id <= 0 ? -1 : e.licence, pays: e.country, nom: e.lname, prenom: e.fname, @@ -805,6 +828,113 @@ function FileOutput({data, data2}) { ); } +function FileImportGuest({data2, sendRegisters}) { + const {t} = useTranslation(); + + const expectedFields = [ + {key: 'nom', label: t('nom'), mandatory: true, type: 'String'}, + {key: 'prenom', label: t('prenom'), mandatory: true, type: 'String'}, + {key: 'pays', label: t('pays'), mandatory: false, type: 'String'}, + {key: 'genre', label: t('genre'), mandatory: false, type: 'String'}, + {key: 'weight', label: t('poids'), mandatory: false, type: 'Integer'}, + {key: 'categorie', label: t('catégorie'), mandatory: true, type: 'String'}, + {key: 'club', label: t('club', {count: 1}), mandatory: false, type: 'String'}, + ]; + + if (data2) + data2.forEach(row => { + expectedFields.push({key: "__" + row.id, label: row.name, mandatory: false, type: 'Boolean'}) + }) + + + const onDataMapped = (mappedData) => { + const out = [] + mappedData.forEach(row => { + if (!row.nom || !row.prenom || !row.categorie) { + toast.warn(t('ligneIgnorée1')) + return; + } + + const categoriesInscrites = [] + data2.forEach(cat => { + if (row["__" + cat.id]) { + categoriesInscrites.push(cat.id) + } + delete row["__" + cat.id] + }) + out.push({ + id: 0, + licence: -1, + fname: row.prenom.trim(), + lname: row.nom.trim(), + country: row.pays ? row.pays.trim() : "FR", + genre: row.genre ? row.genre.trim() : "NA", + categorie: getCatFromName(row.categorie.trim()), + club: row.club ? row.club.trim() : "", + weight: row.weight, + overCategory: 0, + lockEdit: false, + categoriesInscrites: categoriesInscrites + }) + }) + + sendRegisters(out) + } + + return +} + +function FileImportComb({data2, sendRegisters}) { + const {t} = useTranslation(); + + const expectedFields = [ + {key: 'licence', label: t('licence'), mandatory: true, type: 'Integer'}, + {key: 'nom', label: t('nom'), mandatory: true, type: 'String'}, + {key: 'prenom', label: t('prenom'), mandatory: true, type: 'String'}, + {key: 'weight', label: t('poids'), mandatory: false, type: 'Integer'}, + {key: 'overCategory', label: t('comp.modal.surclassement'), mandatory: false, type: 'Integer'}, + ]; + + if (data2) + data2.forEach(row => { + expectedFields.push({key: "__" + row.id, label: row.name, mandatory: false, type: 'Boolean'}) + }) + + const onDataMapped = (mappedData) => { + const out = [] + mappedData.forEach(row => { + if (row.licence && row.licence <= 0) + return; + if (!(row.licence || (row.nom && row.prenom))) { + toast.warn(t('ligneIgnorée2')) + return; + } + + const categoriesInscrites = [] + data2.forEach(cat => { + if (row["__" + cat.id]) { + categoriesInscrites.push(cat.id) + } + delete row["__" + cat.id] + }) + out.push({ + id: 0, + licence: row.licence, + fname: row.prenom.trim(), + lname: row.nom.trim(), + weight: row.weight, + overCategory: row.overCategory, + lockEdit: false, + categoriesInscrites: categoriesInscrites + }) + }) + + sendRegisters(out) + } + + return +} + function Def() { return
  • diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 19434e0..af3c5cf 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -137,6 +137,35 @@ export function getCatName(cat) { } } +export function getCatFromName(name) { + switch (name.toLowerCase()) { + case i18n.t('cat.superMini').toLowerCase(): + return "SUPER_MINI"; + case i18n.t('cat.miniPoussin').toLowerCase(): + return "MINI_POUSSIN"; + case i18n.t('cat.poussin').toLowerCase(): + return "POUSSIN"; + case i18n.t('cat.benjamin').toLowerCase(): + return "BENJAMIN"; + case i18n.t('cat.minime').toLowerCase(): + return "MINIME"; + case i18n.t('cat.cadet').toLowerCase(): + return "CADET"; + case i18n.t('cat.junior').toLowerCase(): + return "JUNIOR"; + case i18n.t('cat.senior1').toLowerCase(): + return "SENIOR1"; + case i18n.t('cat.senior2').toLowerCase(): + return "SENIOR2"; + case i18n.t('cat.vétéran1').toLowerCase(): + return "VETERAN1"; + case i18n.t('cat.vétéran2').toLowerCase(): + return "VETERAN2"; + default: + return name; + } +} + export const SwordList = [ "NONE", "ONE_HAND",