From 11dca5630c05a87a2cedde91e54e1cb6e00caa41 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 20 Aug 2025 16:48:11 +0200 Subject: [PATCH] feat: register for club and free mode wip: helloasso --- .../ffsaf/data/model/CompetitionModel.java | 3 + .../ffsaf/data/model/RegisterModel.java | 3 + .../ffsaf/domain/service/ClubService.java | 18 +- .../domain/service/CompetitionService.java | 213 +++++++--- .../titionfire/ffsaf/rest/ClubEndpoints.java | 17 + .../ffsaf/rest/CompetitionEndpoints.java | 20 +- .../ffsaf/rest/data/RegisterRequestData.java | 1 + .../ffsaf/rest/data/SimpleRegisterComb.java | 4 +- .../ffsaf/rest/data/VerySimpleMembre.java | 18 + .../src/pages/competition/CompetitionList.jsx | 2 +- .../competition/CompetitionRegisterAdmin.css | 36 ++ .../competition/CompetitionRegisterAdmin.jsx | 373 +++++++++++++----- .../src/pages/competition/CompetitionRoot.jsx | 8 +- .../src/pages/competition/CompetitionView.jsx | 118 +++++- 14 files changed, 653 insertions(+), 181 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/VerySimpleMembre.java create mode 100644 src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.css 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 c5701d7..77b4b94 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -9,6 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -54,6 +55,8 @@ public class CompetitionModel { @OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL) List insc; + List banMembre = new ArrayList<>(); + String owner; String data1; 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 57407c2..19f84ab 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -40,6 +40,9 @@ public class RegisterModel { @JoinColumn(name = "club") ClubModel club = null; + @Column(nullable = false, columnDefinition = "boolean default false") + boolean lockEdit = false; + 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/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index 82ff6d9..40a6155 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -10,10 +10,7 @@ import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; import fr.titionfire.ffsaf.net2.request.SReqClub; -import fr.titionfire.ffsaf.rest.data.ClubMapData; -import fr.titionfire.ffsaf.rest.data.DeskMember; -import fr.titionfire.ffsaf.rest.data.RenewAffData; -import fr.titionfire.ffsaf.rest.data.SimpleClubList; +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; @@ -144,6 +141,19 @@ public class ClubService { .toList()); } + public Uni> getMembers(SecurityCtx securityCtx) { + return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult() + .invoke(Unchecked.consumer(m -> { + if (m == null || m.getClub() == null) + throw new DNotFoundException("Club non trouvé"); + if (!securityCtx.isInClubGroup(m.getClub().getId())) + throw new DForbiddenException(); + })) + .chain(m -> combRepository.list("club = ?1", m.getClub())) + .map(membreModels -> membreModels.stream() + .map(m -> new VerySimpleMembre(m.getLname(), m.getFname(), m.getLicence())).toList()); + } + public Uni updateOfUser(SecurityCtx securityCtx, PartClubForm form) { TypeReference> typeRef = new TypeReference<>() { }; 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 f1f6751..f01590a 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -59,6 +59,9 @@ public class CompetitionService { @Inject ServerCustom serverCustom; + @Inject + MembreService membreService; + @Inject CompetPermService permService; @@ -196,6 +199,9 @@ public class CompetitionService { } private void copyData(CompetitionData data, CompetitionModel model) { + if (model.getBanMembre() == null) + model.setBanMembre(new ArrayList<>()); + model.setName(data.getName()); model.setAdresse(data.getAdresse()); model.setDescription(data.getDescription()); @@ -211,55 +217,120 @@ public class CompetitionService { model.setData4(data.getData4()); } - public Uni> getRegister(SecurityCtx securityCtx, Long id) { - return permService.hasEditPerm(securityCtx, id) - .chain(c -> Mutiny.fetch(c.getInsc())) - .onItem().transformToMulti(Multi.createFrom()::iterable) - .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences())) - .map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())) - .collect().asList(); + public Uni> getRegister(SecurityCtx securityCtx, Long id, String source) { + if ("admin".equals(source)) + return permService.hasEditPerm(securityCtx, id) + .chain(c -> Mutiny.fetch(c.getInsc())) + .onItem().transformToMulti(Multi.createFrom()::iterable) + .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences())) + .map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())) + .collect().asList(); + if ("club".equals(source)) + return Uni.createFrom().nullItem() + .invoke(Unchecked.consumer(__ -> { + if (!securityCtx.isClubAdmin()) + throw new DForbiddenException(); + })) + .chain(__ -> membreService.getByAccountId(securityCtx.getSubject())) + .chain(model -> registerRepository.list("competition.id = ?1 AND membre.club = ?2", id, + model.getClub())) + .onItem().transformToMulti(Multi.createFrom()::iterable) + .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences())) + .map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())) + .collect().asList(); + + return membreService.getByAccountId(securityCtx.getSubject()) + .chain(model -> registerRepository.find("competition.id = ?1 AND membre = ?2", id, model).firstResult() + .map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of())))); } - public Uni addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data) { - return permService.hasEditPerm(securityCtx, id) - .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) - .chain(combModel -> Mutiny.fetch(c.getInsc()) - .chain(Unchecked.function(insc -> { - Optional opt = insc.stream() - .filter(m -> m.getMembre().equals(combModel)).findAny(); + public Uni addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data, + String source) { + if ("admin".equals(source)) + return permService.hasEditPerm(securityCtx, id) + .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) + .chain(combModel -> updateRegister(id, data, c, combModel, true))) + .chain(r -> Mutiny.fetch(r.getMembre().getLicences()) + .map(licences -> SimpleRegisterComb.fromModel(r, licences))); + if ("club".equals(source)) + return repository.findById(id) + .invoke(Unchecked.consumer(cm -> { + if (!(cm.getRegisterMode() == RegisterMode.CLUB_ADMIN || cm.getRegisterMode() == RegisterMode.FREE) + || !securityCtx.isClubAdmin()) + throw new DForbiddenException(); + if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) + throw new DBadRequestException("Inscription fermée"); + })) + .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) + .invoke(Unchecked.consumer(model -> { + if (!securityCtx.isInClubGroup(model.getClub().getId())) + throw new DForbiddenException(); + if (c.getBanMembre().contains(model.getId())) + throw new DForbiddenException( + "Vous n'avez pas le droit d'inscrire ce membre (par décision de l'administrateur de la compétition)"); + })) + .chain(combModel -> updateRegister(id, data, c, combModel, false))) + .chain(r -> Mutiny.fetch(r.getMembre().getLicences()) + .map(licences -> SimpleRegisterComb.fromModel(r, licences))); - RegisterModel r; - if (opt.isPresent()) { - r = opt.get(); - r.setWeight(data.getWeight()); - r.setOverCategory(data.getOverCategory()); - r.setCategorie( - (combModel.getBirth_date() == null) ? combModel.getCategorie() : - Utils.getCategoryFormBirthDate(combModel.getBirth_date(), - c.getDate())); - int days = Utils.getDaysBeforeCompetition(c.getDate()); - if (days > -7) { - r.setClub(combModel.getClub()); - } - } 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()); - insc.add(r); - } + return repository.findById(id) + .invoke(Unchecked.consumer(cm -> { + if (cm.getRegisterMode() != RegisterMode.FREE) + throw new DForbiddenException(); + if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) + throw new DBadRequestException("Inscription fermée"); + })) + .chain(c -> membreService.getByAccountId(securityCtx.getSubject()) + .invoke(Unchecked.consumer(model -> { + if (c.getBanMembre().contains(model.getId())) + throw new DForbiddenException( + "Vous n'avez pas le droit de vous inscrire (par décision de l'administrateur de la compétition)"); + })) + .chain(combModel -> updateRegister(id, data, c, combModel, false))) + .map(r -> SimpleRegisterComb.fromModel(r, List.of())); + } - if (c.getSystem() == CompetitionSystem.SAFCA) { - SReqRegister.sendIfNeed(serverCustom.clients, - new CompetitionData.SimpleRegister(r.getMembre().getId(), - r.getOverCategory(), r.getWeight(), r.getCategorie(), - (r.getClub() == null) ? null : r.getClub().getId()), c.getId()); - } - return Panache.withTransaction(() -> repository.persist(c)).map(__ -> r); - })))) - .chain(r -> Mutiny.fetch(r.getMembre().getLicences()) - .map(licences -> SimpleRegisterComb.fromModel(r, licences))); + private Uni updateRegister(Long id, RegisterRequestData data, CompetitionModel c, + MembreModel combModel, boolean admin) { + return registerRepository.find("competition.id = ?1 AND membre = ?2", id, combModel).firstResult() + .onFailure().recoverWithNull() + .map(Unchecked.function(r -> { + if (r != null) { + if (!admin && r.isLockEdit()) + throw new DForbiddenException( + "Modification bloquée par l'administrateur de la compétition"); + r.setWeight(data.getWeight()); + r.setOverCategory(data.getOverCategory()); + r.setCategorie( + (combModel.getBirth_date() == null) ? combModel.getCategorie() : + Utils.getCategoryFormBirthDate(combModel.getBirth_date(), + c.getDate())); + int days = Utils.getDaysBeforeCompetition(c.getDate()); + if (days > -7) + r.setClub(combModel.getClub()); + if (admin) + 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) + r.setLockEdit(data.isLockEdit()); + 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.getClub() == null) ? null : r.getClub().getId()), c.getId()); + } + return r; + })) + .chain(r -> Panache.withTransaction(() -> registerRepository.persist(r))); } private Uni findComb(Long licence, String fname, String lname) { @@ -282,19 +353,45 @@ public class CompetitionService { } } - public Uni removeRegisterComb(SecurityCtx securityCtx, Long id, Long combId) { - return permService.hasEditPerm(securityCtx, id) - .chain(c -> registerRepository.delete("competition = ?1 AND membre.id = ?2", c, combId) - .invoke(Unchecked.consumer(l -> { - if (l != 0) { - if (c.getSystem() == CompetitionSystem.SAFCA) { - SReqRegister.sendRmIfNeed(serverCustom.clients, combId, id); - } - } else { - throw new DBadRequestException("Combattant non inscrit"); - } - })) - ).replaceWithVoid(); + public Uni removeRegisterComb(SecurityCtx securityCtx, Long id, Long combId, String source) { + if ("admin".equals(source)) + return permService.hasEditPerm(securityCtx, id) + .chain(c -> deleteRegister(combId, c, true)); + if ("club".equals(source)) + return repository.findById(id) + .invoke(Unchecked.consumer(cm -> { + if (!(cm.getRegisterMode() == RegisterMode.CLUB_ADMIN || cm.getRegisterMode() == RegisterMode.FREE) + || !securityCtx.isClubAdmin()) + throw new DForbiddenException(); + if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) + throw new DBadRequestException("Inscription fermée"); + })) + .call(cm -> membreService.getByAccountId(securityCtx.getSubject()) + .invoke(Unchecked.consumer(model -> { + if (!securityCtx.isInClubGroup(model.getClub().getId())) + throw new DForbiddenException(); + }))) + .chain(c -> deleteRegister(combId, c, false)); + + return repository.findById(id) + .invoke(Unchecked.consumer(cm -> { + if (cm.getRegisterMode() != RegisterMode.FREE) + throw new DForbiddenException(); + if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) + throw new DBadRequestException("Inscription fermée"); + })) + .chain(c -> deleteRegister(combId, c, false)); + } + + private Uni deleteRegister(Long combId, CompetitionModel c, boolean admin) { + return registerRepository.find("competition = ?1 AND membre.id = ?2", c, combId).firstResult() + .onFailure().transform(t -> new DBadRequestException("Combattant non inscrit")) + .call(Unchecked.function(registerModel -> { + if (!admin && registerModel.isLockEdit()) + throw new DForbiddenException("Modification bloquée par l'administrateur de la compétition"); + return Panache.withTransaction(() -> registerRepository.delete(registerModel)); + })) + .replaceWithVoid(); } public Uni delete(SecurityCtx securityCtx, Long id) { diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 1b3a2c2..d4dcf09 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -275,6 +275,23 @@ public class ClubEndpoints { return pdfService.getAffiliationPdf(securityCtx.getSubject()); } + + @GET + @Path("/members") + @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra", "club_tresorier"}) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Revoie tout les membres de votre club") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "List des membres"), + @APIResponse(responseCode = "403", description = "Accès refusé"), + @APIResponse(responseCode = "404", description = "L'utilisateur n'est pas membre d'un club"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Uni> getMembers() { + return clubService.getMembers(securityCtx); + } + + @GET @Path("/renew/{id}") @RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java index 3d2398d..fe74f04 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java @@ -36,29 +36,31 @@ public class CompetitionEndpoints { } @GET - @Path("{id}/register") + @Path("{id}/register/{source}") @Authenticated @Produces(MediaType.APPLICATION_JSON) - public Uni> getRegister(@PathParam("id") Long id) { - return service.getRegister(securityCtx, id); + public Uni> getRegister(@PathParam("id") Long id, @PathParam("source") String source) { + return service.getRegister(securityCtx, id, source); } @POST - @Path("{id}/register") + @Path("{id}/register/{source}") @Authenticated @Produces(MediaType.APPLICATION_JSON) @Operation(hidden = true) - public Uni addRegisterComb(@PathParam("id") Long id, RegisterRequestData data) { - return service.addRegisterComb(securityCtx, id, data); + public Uni addRegisterComb(@PathParam("id") Long id, @PathParam("source") String source, + RegisterRequestData data) { + return service.addRegisterComb(securityCtx, id, data, source); } @DELETE - @Path("{id}/register/{comb_id}") + @Path("{id}/register/{comb_id}/{source}") @Authenticated @Produces(MediaType.APPLICATION_JSON) @Operation(hidden = true) - public Uni removeRegisterComb(@PathParam("id") Long id, @PathParam("comb_id") Long combId) { - return service.removeRegisterComb(securityCtx, id, combId); + public Uni removeRegisterComb(@PathParam("id") Long id, @PathParam("comb_id") Long combId, + @PathParam("source") String source) { + return service.removeRegisterComb(securityCtx, id, combId, source); } @GET 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 74811d8..eb9c50b 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -12,4 +12,5 @@ public class RegisterRequestData { private Integer weight; private int overCategory; + private boolean lockEdit = 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 7026f65..2075f68 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java @@ -24,6 +24,7 @@ public class SimpleRegisterComb { private Integer weight; private int overCategory; private boolean hasLicenceActive; + private boolean lockEdit; public static SimpleRegisterComb fromModel(RegisterModel register, List licences) { MembreModel membreModel = register.getMembre(); @@ -31,6 +32,7 @@ public class SimpleRegisterComb { (register.getCategorie() == null) ? "Catégorie inconnue" : register.getCategorie().getName(), SimpleClubModel.fromModel(register.getClub()), membreModel.getLicence(), register.getWeight(), register.getOverCategory(), - licences.stream().anyMatch(l -> l.isValidate() && l.getSaison() == Utils.getSaison())); + licences.stream().anyMatch(l -> l.isValidate() && l.getSaison() == Utils.getSaison()), + register.isLockEdit()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/VerySimpleMembre.java b/src/main/java/fr/titionfire/ffsaf/rest/data/VerySimpleMembre.java new file mode 100644 index 0000000..cbf8931 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/VerySimpleMembre.java @@ -0,0 +1,18 @@ +package fr.titionfire.ffsaf.rest.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class VerySimpleMembre { + @Schema(description = "Le nom du membre.", example = "Dupont") + private String lname = ""; + @Schema(description = "Le prénom du membre.", example = "Jean") + private String fname = ""; + @Schema(description = "Le numéro de licence du membre.", example = "12345") + private Integer licence; +} diff --git a/src/main/webapp/src/pages/competition/CompetitionList.jsx b/src/main/webapp/src/pages/competition/CompetitionList.jsx index 0541147..dad9d94 100644 --- a/src/main/webapp/src/pages/competition/CompetitionList.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionList.jsx @@ -58,7 +58,7 @@ const inscText = (type) => { function MakeRow({data, navigate}) { return
data.canEdit ? navigate("" + data.id) : navigate("view/" + data.id)}> + onClick={() => data.canEdit ? navigate("" + data.id) : navigate("" + data.id + "/view")}>
{data.name} par {data.clubName}
diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.css b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.css new file mode 100644 index 0000000..4ecee85 --- /dev/null +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.css @@ -0,0 +1,36 @@ +.autocomplete-wrapper { + position: relative; + width: 100%; + max-width: 500px; +} + +.suggestions-list2 { + position: absolute; + width: 100%; + z-index: 1000; + max-height: 200px; + overflow-y: auto; + margin: 0; + padding: 0; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 4px 4px; +} + +.suggestions-list { + position: absolute; + z-index: 1000; + margin: 0; + padding: 0; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 4px 4px; +} + +.suggestions-list .list-group-item { + cursor: pointer; +} + +.suggestions-list .list-group-item:hover { + background-color: #f8f9fa; +} diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index 74f515a..25106bb 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -1,16 +1,17 @@ import {useNavigate, useParams} from "react-router-dom"; -import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; +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, useState} from "react"; +import {useEffect, useReducer, useRef, useState} from "react"; import {apiAxios} from "../../utils/Tools.js"; import {toast} from "react-toastify"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faTrashCan} from "@fortawesome/free-solid-svg-icons"; +import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons"; +import "./CompetitionRegisterAdmin.css" -export function CompetitionRegisterAdmin() { +export function CompetitionRegisterAdmin({source}) { const {id} = useParams() const navigate = useNavigate() const [state, dispatch] = useReducer(SimpleReducer, []) @@ -19,32 +20,24 @@ export function CompetitionRegisterAdmin() { const [modalState, setModalState] = useState({}) const setLoading = useLoadingSwitcher() - const {data, error} = useFetch(`/competition/${id}/register`, setLoading, 1) + const {data, error} = useFetch(`/competition/${id}/register/${source}`, setLoading, 1) const sortName = (a, b) => { - if (a.data.fname === b.data.fname) - return a.data.lname.localeCompare(b.data.lname); + if (a.data.fname === b.data.fname) return a.data.lname.localeCompare(b.data.lname); return a.data.fname.localeCompare(b.data.fname); } useEffect(() => { - if (!data) - return; - data.forEach((d, index) => { - dispatch({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}}) + if (!data) return; + data.forEach((d) => { + dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d}}) }) dispatch({type: 'SORT', payload: sortName}) }, [data, clubFilter, catFilter]); - const sendRegister = (event, new_state) => { - event.preventDefault(); - - console.log(new_state) - - toast.promise(apiAxios.post(`/competition/${id}/register`, new_state), { - pending: "Recherche en cours", - success: "Combattant trouvé et ajouté/mis à jour", - error: { + const sendRegister = (new_state) => { + toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), { + pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: { render({data}) { return data.response.data || "Combattant non trouvé" } @@ -53,17 +46,7 @@ export function CompetitionRegisterAdmin() { if (response.data.error) { return } - - let maxId = 0; - if (new_state.id) { - maxId = new_state.id - 1 - } else { - state.forEach((d) => { - if (d.id > maxId) - maxId = d.id; - }) - } - dispatch({type: 'UPDATE_OR_ADD', payload: {id: maxId + 1, data: response.data}}) + dispatch({type: 'UPDATE_OR_ADD', payload: {id: response.data.id, data: response.data}}) dispatch({type: 'SORT', payload: sortName}) document.getElementById("closeModal").click(); }) @@ -71,22 +54,18 @@ export function CompetitionRegisterAdmin() { return

Combattants inscrits

-
- {data - ?
- (clubFilter.length === 0 || s.data.club.name === clubFilter) && (catFilter.length === 0 || s.data.categorie === catFilter))} - dispatch={dispatch} id={id} setModalState={setModalState}/> -
- : error - ? - : - } + {data ?
+ (clubFilter.length === 0 || s.data.club.name === clubFilter) && (catFilter.length === 0 || s.data.categorie === catFilter))} + dispatch={dispatch} id={id} setModalState={setModalState} source={source}/> +
: error ? : }
@@ -94,27 +73,213 @@ export function CompetitionRegisterAdmin() { onClick={() => setModalState({})}>Ajouter un combattant
+
Filtre
+ setCatFilter={setCatFilter} source={source}/>
- +
} -function Modal({sendRegister, modalState, setModalState}) { +function QuickAdd({sendRegister, source}) { + + const handleAdd = (licence) => { + console.log("Quick add licence: " + licence) + + sendRegister({ + licence: licence, fname: "", lname: "", weight: "", overCategory: 0, lockEdit: false, id: null + }) + } + + return
+
Ajout rapide
+
+
+ N° de licence +
+
+ { + if (e.key === "Enter") { + const licence = e.target.value.trim() + if (licence.length === 0) return; + e.target.value = "" + handleAdd(licence) + } + }}/> + + + {source === "club" && + + } +
+
+
+} + +function SearchMember({sendRegister}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/club/members`, setLoading, 1) + const [suggestions, setSuggestions] = useState([]) + + const handleAdd = (name) => { + const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name); + console.log("Quick add licence:", member) + + if (!member) { + toast.error("Combattant non trouvé"); + return; + } + + sendRegister({ + licence: member.licence, fname: member.fname, lname: member.lname, weight: "", overCategory: 0, lockEdit: false, id: null + }) + } + + useEffect(() => { + if (!data) return; + + const names = data.map(member => `${member.fname} ${member.lname}`.trim()); + names.sort((a, b) => a.localeCompare(b)); + setSuggestions(names); + }, []); + + return <> + {data ?
+ Prénom et nom + +
: error ? : } + +} + +const AutoCompleteInput = ({suggestions = [], handleAdd}) => { + const [inputValue, setInputValue] = useState(''); + const [filteredSuggestions, setFilteredSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const [activeSuggestion, setActiveSuggestion] = useState(0); + const wrapperRef = useRef(null); + + // Filtre les suggestions + useEffect(() => { + if (inputValue.trim() === '') { + setFilteredSuggestions([]); + setShowSuggestions(false); + } else { + const filtered = suggestions.filter(suggestion => suggestion.toLowerCase().includes(inputValue.toLowerCase())); + setFilteredSuggestions(filtered); + setShowSuggestions(true); + setActiveSuggestion(0); // Réinitialise la sélection active + } + }, [inputValue, suggestions]); + + // Ferme les suggestions si clic à l'extérieur + useEffect(() => { + const handleClickOutside = (event) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { + setShowSuggestions(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Gestion du clic sur une suggestion + const handleSuggestionClick = (suggestion) => { + setInputValue(suggestion); + setShowSuggestions(false); // Ferme automatiquement après sélection + }; + + // Navigation clavier + const handleKeyDown = (e) => { + // Touches directionnelles + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActiveSuggestion(prev => prev < filteredSuggestions.length - 1 ? prev + 1 : prev); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActiveSuggestion(prev => (prev > 0 ? prev - 1 : 0)); + } + // Validation avec Entrée + else if (e.key === 'Enter' && filteredSuggestions.length > 0) { + e.preventDefault(); + if (inputValue === filteredSuggestions[activeSuggestion]) { + handleAdd(inputValue); + setInputValue(''); + } else { + setInputValue(filteredSuggestions[activeSuggestion]); + } + setShowSuggestions(false); + } + // Fermeture avec Échap + else if (e.key === 'Escape') { + setShowSuggestions(false); + } + }; + + return (
+
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + onFocus={() => inputValue && setShowSuggestions(true)} + placeholder="Rechercher..." + aria-autocomplete="list" + aria-expanded={showSuggestions} + aria-controls="suggestions-list" + /> + + +
+ {showSuggestions && filteredSuggestions.length > 0 && (
    + {filteredSuggestions.map((suggestion, index) => (
  • handleSuggestionClick(suggestion)} + role="option" + aria-selected={index === activeSuggestion} + > + {suggestion} +
  • ))} +
)} +
); +}; + +function Modal({sendRegister, modalState, setModalState, source}) { const [licence, setLicence] = useState("") const [fname, setFname] = useState("") const [lname, setLname] = useState("") const [weight, setWeight] = useState("") const [cat, setCat] = useState(0) const [editMode, setEditMode] = useState(false) + const [lockEdit, setLockEdit] = useState(false) useEffect(() => { if (!modalState) { @@ -124,6 +289,7 @@ function Modal({sendRegister, modalState, setModalState}) { setWeight("") setCat(0) setEditMode(false) + setLockEdit(false) } else { setLicence(modalState.licence ? modalState.licence : "") setFname(modalState.fname ? modalState.fname : "") @@ -131,6 +297,7 @@ function Modal({sendRegister, modalState, setModalState}) { setWeight(modalState.weight ? modalState.weight : "") setCat(modalState.overCategory ? modalState.overCategory : 0) setEditMode(modalState.licence || (modalState.fname && modalState.lname)) + setLockEdit(modalState.lockEdit) } }, [modalState]); @@ -139,21 +306,16 @@ function Modal({sendRegister, modalState, setModalState}) {
{ + e.preventDefault() const new_state = { - licence: licence, - fname: fname, - lname: lname, - weight: weight, - overCategory: cat, - id: modalState.id + licence: licence, fname: fname, lname: lname, weight: weight, overCategory: cat, lockEdit: lockEdit, id: modalState.id } setModalState(new_state) - sendRegister(e, new_state) + sendRegister(new_state) }}>
-

Ajouter un combattant

- +

{editMode ? "Modification d'" : "Ajouter "}un combattant

+
@@ -194,9 +356,16 @@ function Modal({sendRegister, modalState, setModalState}) {
+ + {editMode && source === "admin" &&
+ setLockEdit(e.target.checked)}/> + +
}
- +
@@ -208,10 +377,9 @@ function Modal({sendRegister, modalState, setModalState}) { let allClub = [] let allCat = [] -function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter}) { +function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, source}) { useEffect(() => { - if (!data) - return; + if (!data) return; allClub.push(...data.map((e) => e.club?.name)) allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() allCat.push(...data.map((e) => e.categorie)) @@ -219,71 +387,72 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter}) { }, [data]); return
-
+ {source === "admin" &&
-
+
}
} -function MakeCentralPanel({data, dispatch, id, setModalState}) { +function MakeCentralPanel({data, dispatch, id, setModalState, source}) { return <>
- {data.map((req, index) => ( -
setModalState({...req.data, id: req.id})}> -
- {req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"} -
-
{req.data.fname} {req.data.lname}
- {req.data.club?.name || "Sans club"} + {data.map((req, index) => (
+
+
setModalState({...req.data, id: req.id})}> +
+ {req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"} +
+
{req.data.fname} {req.data.lname}
+ {req.data.club?.name || "Sans club"} +
+
+
+
+ {req.data.categorie + (req.data.overCategory === 0 ? "" : (" avec " + req.data.overCategory + " de surclassement"))}
+ {req.data.weight ? req.data.weight : "---"} kg +
+
-
-
- {req.data.categorie + (req.data.overCategory === 0 ? "" : (" avec " + req.data.overCategory + " de surclassement"))}
- {req.data.weight ? req.data.weight : "---"} kg -
-
-
- -
+ } + }).finally(() => { + dispatch({type: 'REMOVE', payload: req.id}) + }) + }}> + +
- ))} +
))}
@@ -297,4 +466,4 @@ function Def() {
  • -} \ No newline at end of file +} diff --git a/src/main/webapp/src/pages/competition/CompetitionRoot.jsx b/src/main/webapp/src/pages/competition/CompetitionRoot.jsx index bf02c63..6daa71c 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRoot.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRoot.jsx @@ -25,12 +25,16 @@ export function getCompetitionChildren() { element: }, { - path: 'view/:id', + path: ':id/view', element: }, { path: ':id/register', - element: + element: + }, + { + path: ':id/club/register', + element: } ] } diff --git a/src/main/webapp/src/pages/competition/CompetitionView.jsx b/src/main/webapp/src/pages/competition/CompetitionView.jsx index cf2a6e2..c179f73 100644 --- a/src/main/webapp/src/pages/competition/CompetitionView.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionView.jsx @@ -3,7 +3,10 @@ import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; import {useFetch} from "../../hooks/useFetch.js"; import {AxiosError} from "../../components/AxiosError.jsx"; import {useAuth} from "../../hooks/useAuth.jsx"; -import {isClubAdmin} from "../../utils/Tools.js"; +import {apiAxios, isClubAdmin} from "../../utils/Tools.js"; +import {ThreeDots} from "react-loader-spinner"; +import {useEffect, useState} from "react"; +import {toast} from "react-toastify"; export function CompetitionView() { @@ -43,6 +46,7 @@ const inscText = (type) => { function MakeContent({data}) { const {userinfo} = useAuth() + const navigate = useNavigate() return
    @@ -57,10 +61,18 @@ function MakeContent({data}) {

    Organisateur : {data.clubName}

    Type d'inscription : {inscText(data.registerMode)}

    {(data.registerMode === "FREE" || data.registerMode === "CLUB_ADMIN") && -

    Date d'inscription : Du {new Date(data.startRegister.split('+')[0]).toLocaleString()} au {new Date(data.endRegister.split('+')[0]).toLocaleString()}

    +

    Date d'inscription + : Du {new Date(data.startRegister.split('+')[0]).toLocaleString()} au {new Date(data.endRegister.split('+')[0]).toLocaleString()} +

    } - {(data.registerMode === "CLUB_ADMIN" && isClubAdmin(userinfo)) || data.registerMode === "FREE" && - + + {(data.registerMode === "FREE" || data.registerMode === "CLUB_ADMIN") && isClubAdmin(userinfo) && + + } + {data.registerMode === "FREE" && !isClubAdmin(userinfo) && + } {data.registerMode === "HELLOASSO" &&

    Billetterie :

    } + +function SelfRegister({data2}) { + const {id} = useParams() + const setLoading = useLoadingSwitcher() + const {data, refresh, error} = useFetch(`/competition/${id}/register/user`, setLoading, 1) + + const [weight, setWeight] = useState("") + const [cat, setCat] = useState(0) + + useEffect(() => { + if (data && data.length > 0) { + setWeight(data[0].weight || "") + setCat(data[0].overCategory || 0) + } + }, [data]); + + const disabled = new Date() < new Date(data2.startRegister.split('+')[0]) || new Date() > new Date(data2.endRegister.split('+')[0]) + + const handleUnregister = () => { + if (window.confirm("Êtes-vous sûr de vouloir vous désinscrire ?")) { + toast.promise(apiAxios.delete(`/competition/${id}/register/${data[0].id}/user`), { + pending: "Désinscription en cours", + success: "Désinscription réalisée", + error: { + render({data}) { + return data.response.data || "Erreur" + } + } + }).finally(() => { + refresh(`/competition/${id}/register/user`) + }) + } + } + + const sendSubmit = (new_state) => { + toast.promise(apiAxios.post(`/competition/${id}/register/user`, new_state), { + pending: "Enregistrement en cours", + success: "Inscription réalisée", + error: { + render({data}) { + return data.response.data || "Combattant non trouvé" + } + } + }).finally(() => { + refresh(`/competition/${id}/register/user`) + }) + } + + const handleSubmit = (e) => { + sendSubmit({ + licence: 0, fname: "", lname: "", weight: weight, overCategory: cat, lockEdit: false, id: null + }) + } + + return <> + {data + ? data.length > 0 + ?
    +

    Mon inscription

    +
    + Poids (en kg) + setWeight(e.target.value)}/> +
    + +
    Catégorie normalisée: {data[0].categorie}
    +
    + Surclassement + +
    + +
    + + +
    +
    + : + : error + ? + : + } + +} + +function Def() { + return
    +
  • +
    +}