From 3f1fddccd13abc736495e5b29bb0cdb7becb2b5a Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 11 Feb 2025 21:23:03 +0100 Subject: [PATCH 1/9] feat(insc): add frontend --- .../ffsaf/data/model/CompetitionModel.java | 10 +- .../domain/service/CompetitionService.java | 116 +++++-- .../ffsaf/rest/CompetitionEndpoints.java | 29 ++ .../ffsaf/rest/data/RegisterRequestData.java | 15 + .../ffsaf/rest/data/SimpleRegisterComb.java | 36 +++ .../ffsaf/utils/RegisterEmbeddable.java | 28 ++ .../src/pages/competition/CompetitionEdit.jsx | 11 +- .../competition/CompetitionRegisterAdmin.jsx | 289 ++++++++++++++++++ .../src/pages/competition/CompetitionRoot.jsx | 5 + 9 files changed, 513 insertions(+), 26 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java create mode 100644 src/main/java/fr/titionfire/ffsaf/utils/RegisterEmbeddable.java create mode 100644 src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx 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 c9f6f15..59a210e 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/CompetitionModel.java @@ -1,6 +1,7 @@ package fr.titionfire.ffsaf.data.model; import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.RegisterEmbeddable; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.AllArgsConstructor; @@ -37,12 +38,9 @@ public class CompetitionModel { Date date; - @ManyToMany - @JoinTable(name = "register", - uniqueConstraints = @UniqueConstraint(columnNames = {"id_competition", "id_membre"}), - joinColumns = @JoinColumn(name = "id_competition"), - inverseJoinColumns = @JoinColumn(name = "id_membre")) - List insc; + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "register", joinColumns = @JoinColumn(name = "id_competition")) + List insc; String owner; } 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 3a338ac..6433ffe 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -1,26 +1,31 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.CompetitionModel; -import fr.titionfire.ffsaf.data.repository.ClubRepository; -import fr.titionfire.ffsaf.data.repository.CompetitionRepository; -import fr.titionfire.ffsaf.data.repository.MatchRepository; -import fr.titionfire.ffsaf.data.repository.PouleRepository; +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.data.SimpleCompet; import fr.titionfire.ffsaf.net2.request.SReqCompet; 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.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.utils.CompetitionSystem; +import fr.titionfire.ffsaf.utils.RegisterEmbeddable; import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.cache.Cache; +import io.quarkus.cache.CacheName; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import io.vertx.mutiny.core.Vertx; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.hibernate.reactive.mutiny.Mutiny; import org.keycloak.representations.idm.UserRepresentation; import java.util.*; @@ -42,6 +47,9 @@ public class CompetitionService { @Inject KeycloakService keycloakService; + @Inject + CombRepository combRepository; + @Inject ServerCustom serverCustom; @@ -51,6 +59,14 @@ public class CompetitionService { @Inject Vertx vertx; + @Inject + @CacheName("safca-config") + Cache cache; + + @Inject + @CacheName("safca-have-access") + Cache cacheAccess; + public Uni getById(SecurityCtx securityCtx, Long id) { if (id == 0) { return Uni.createFrom() @@ -138,7 +154,7 @@ public class CompetitionService { return Panache.withTransaction(() -> repository.persist(model)); }).map(CompetitionData::fromModel) - .call(__ -> permService.cacheAccess.invalidate(securityCtx.getSubject())); + .call(__ -> cacheAccess.invalidate(securityCtx.getSubject())); } else { return permService.hasEditPerm(securityCtx, data.getId()) .chain(model -> { @@ -160,10 +176,71 @@ public class CompetitionService { })) .chain(__ -> Panache.withTransaction(() -> repository.persist(model))); }).map(CompetitionData::fromModel) - .call(__ -> permService.cacheAccess.invalidate(securityCtx.getSubject())); + .call(__ -> cacheAccess.invalidate(securityCtx.getSubject())); } } + 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 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(); + + RegisterEmbeddable r; + if (opt.isPresent()) { + r = opt.get(); + r.setWeight(data.getWeight()); + r.setOverCategory(data.getOverCategory()); + } else { + r = new RegisterEmbeddable(combModel, data.getWeight(), data.getOverCategory()); + insc.add(r); + } + return Panache.withTransaction(() -> repository.persist(c)).map(__ -> r); + })))) + .chain(r -> Mutiny.fetch(r.getMembre().getLicences()) + .map(licences -> SimpleRegisterComb.fromModel(r, licences))); + } + + private Uni findComb(Long licence, String fname, String lname) { + if (licence != null && licence != 0) { + return combRepository.find("licence = ?1", licence).firstResult() + .invoke(Unchecked.consumer(combModel -> { + if (combModel == null) + throw new DForbiddenException("Licence " + licence + " non trouvé"); + })); + } else { + if (fname == null || lname == null) + return Uni.createFrom().failure(new DBadRequestException("Nom et prénom requis")); + return combRepository.find("LOWER(lname) LIKE LOWER(?1) OR LOWER(fname) LIKE LOWER(?2)", lname, + fname).firstResult() + .invoke(Unchecked.consumer(combModel -> { + if (combModel == null) + throw new DForbiddenException("Combattant " + fname + " " + lname + " non trouvé"); + })); + } + } + + public Uni removeRegisterComb(SecurityCtx securityCtx, Long id, Long combId) { + return permService.hasEditPerm(securityCtx, id) + .chain(c -> Mutiny.fetch(c.getInsc()) + .chain(Unchecked.function(insc -> { + if (insc.removeIf(m -> m.getMembre().getId().equals(combId))) + return Panache.withTransaction(() -> repository.persist(c)).map(__ -> null); + throw new DBadRequestException("Combattant non inscrit"); + }))); + } + public Uni delete(SecurityCtx securityCtx, Long id) { return repository.findById(id).invoke(Unchecked.consumer(c -> { if (!securityCtx.getSubject().equals(c.getOwner()) || securityCtx.roleHas("federation_admin")) @@ -180,7 +257,7 @@ public class CompetitionService { () -> pouleRepository.delete("compet = ?1", competitionModel))) .chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId()))) .invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id)) - .call(__ -> permService.cache.invalidate(id)); + .call(__ -> cache.invalidate(id)); } public Uni getSafcaData(SecurityCtx securityCtx, Long id) { @@ -234,15 +311,20 @@ public class CompetitionService { })) .invoke(simpleCompet -> SReqCompet.sendUpdate(serverCustom.clients, simpleCompet)) .call(simpleCompet -> permService.getSafcaConfig(data.getId()) - .call(c -> Uni.join().all(Stream.concat( - Stream.concat( - c.admin().stream().filter(uuid -> !simpleCompet.admin().contains(uuid)), - simpleCompet.admin().stream().filter(uuid -> !c.admin().contains(uuid))), - Stream.concat( - c.table().stream().filter(uuid -> !simpleCompet.table().contains(uuid)), - simpleCompet.table().stream().filter(uuid -> !c.table().contains(uuid)))) - .map(uuid -> permService.cacheAccess.invalidate(uuid.toString())).toList()) - .andCollectFailures())) - .call(__ -> permService.cache.invalidate(data.getId())); + .call(c -> { + List> list = Stream.concat( + Stream.concat( + c.admin().stream().filter(uuid -> !simpleCompet.admin().contains(uuid)), + simpleCompet.admin().stream().filter(uuid -> !c.admin().contains(uuid))), + Stream.concat( + c.table().stream().filter(uuid -> !simpleCompet.table().contains(uuid)), + simpleCompet.table().stream().filter(uuid -> !c.table().contains(uuid))) + ).map(uuid -> cacheAccess.invalidate(uuid.toString())).toList(); + + if (list.isEmpty()) + return Uni.createFrom().nullItem(); + return Uni.join().all(list).andCollectFailures(); + })) + .call(__ -> cache.invalidate(data.getId())); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java index 3cde90e..b54abd2 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CompetitionEndpoints.java @@ -2,7 +2,9 @@ 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.utils.CompetitionSystem; import fr.titionfire.ffsaf.utils.SecurityCtx; import io.quarkus.security.Authenticated; @@ -10,6 +12,7 @@ import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; import java.util.List; @@ -30,6 +33,32 @@ public class CompetitionEndpoints { return service.getById(securityCtx, id); } + @GET + @Path("{id}/register") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Uni> getRegister(@PathParam("id") Long id) { + return service.getRegister(securityCtx, id); + } + + @POST + @Path("{id}/register") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Operation(hidden = true) + public Uni addRegisterComb(@PathParam("id") Long id, RegisterRequestData data) { + return service.addRegisterComb(securityCtx, id, data); + } + + @DELETE + @Path("{id}/register/{comb_id}") + @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); + } + @GET @Path("{id}/safcaData") @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 new file mode 100644 index 0000000..74811d8 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RegisterRequestData.java @@ -0,0 +1,15 @@ +package fr.titionfire.ffsaf.rest.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; + +@Data +@RegisterForReflection +public class RegisterRequestData { + private Long licence; + private String fname; + private String lname; + + private Integer weight; + private int overCategory; +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java new file mode 100644 index 0000000..0a8dfec --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleRegisterComb.java @@ -0,0 +1,36 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.LicenceModel; +import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.net2.data.SimpleClubModel; +import fr.titionfire.ffsaf.utils.RegisterEmbeddable; +import fr.titionfire.ffsaf.utils.Utils; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@RegisterForReflection +public class SimpleRegisterComb { + private long id; + private String fname; + private String lname; + private String categorie; + private SimpleClubModel club; + private Integer licence; + private Integer weight; + private int overCategory; + private boolean hasLicenceActive; + + public static SimpleRegisterComb fromModel(RegisterEmbeddable register, List licences) { + MembreModel membreModel = register.getMembre(); + return new SimpleRegisterComb(membreModel.getId(), membreModel.getFname(), membreModel.getLname(), + (membreModel.getCategorie() == null) ? "Catégorie inconnue" : membreModel.getCategorie().getName(), + SimpleClubModel.fromModel(membreModel.getClub()), membreModel.getLicence(), register.getWeight(), + register.getOverCategory(), + licences.stream().anyMatch(l -> l.isValidate() && l.getSaison() == Utils.getSaison())); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/RegisterEmbeddable.java b/src/main/java/fr/titionfire/ffsaf/utils/RegisterEmbeddable.java new file mode 100644 index 0000000..5258b42 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/RegisterEmbeddable.java @@ -0,0 +1,28 @@ +package fr.titionfire.ffsaf.utils; + +import fr.titionfire.ffsaf.data.model.MembreModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.persistence.Embeddable; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection + +@Embeddable +public class RegisterEmbeddable { + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "id_membre") + MembreModel membre; + + Integer weight; + int overCategory = 0; +} diff --git a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx index 8deebcb..27e24b3 100644 --- a/src/main/webapp/src/pages/competition/CompetitionEdit.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionEdit.jsx @@ -43,8 +43,12 @@ export function CompetitionEdit() {
{data ?
+ + {data.id !== null && } + {data.id !== null && } {data.id !== null && <> @@ -74,14 +78,14 @@ function ContentSAFCA({data2}) { useEffect(() => { if (data === null) return - if (data.admin !== null){ + if (data.admin !== null) { let index = 0 for (const d of data.admin) { dispatch({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}}) index++ } } - if (data.table !== null){ + if (data.table !== null) { let index = 0 for (const d of data.table) { dispatch2({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}}) @@ -232,7 +236,8 @@ function Content({data}) { }, } ).then(data => { - navigate("/competition/" + data.id) + if (data.id !== undefined) + navigate("/competition/" + data.id) }) } diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx new file mode 100644 index 0000000..cf202c6 --- /dev/null +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -0,0 +1,289 @@ +import {useNavigate, useParams} from "react-router-dom"; +import {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 {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"; + +export function CompetitionRegisterAdmin() { + const {id} = useParams() + const navigate = useNavigate() + const [state, dispatch] = useReducer(SimpleReducer, []) + const [clubFilter, setClubFilter] = useState("") + const [catFilter, setCatFilter] = useState("") + const [modalState, setModalState] = useState({}) + + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/competition/${id}/register`, setLoading, 1) + + useEffect(() => { + if (!data) + return; + data.forEach((d, index) => { + dispatch({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}}) + }) + }, [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: { + render({data}) { + return data.response.data || "Combattant non trouvé" + } + } + }).then((response) => { + 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}}) + document.getElementById("closeModal").click(); + }) + } + + 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 + ? + : + } +
+
+
+ +
+
+
Filtre
+
+ +
+
+
+
+ + +
+} + +function Modal({sendRegister, modalState, setModalState}) { + 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) + + useEffect(() => { + if (!modalState) { + setLicence("") + setFname("") + setLname("") + setWeight("") + setCat(0) + setEditMode(false) + } 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)) + } + }, [modalState]); + + return +} + +let allClub = [] +let allCat = [] + +function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter}) { + useEffect(() => { + 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)) + allCat = allCat.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() + }, [data]); + + return
+
+ +
+
+ +
+
+} + +function MakeCentralPanel({data, dispatch, id, setModalState}) { + 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"}
{req.data.categorie}
+
+
+ +
+
+
+ ))} +
+
+ +} + +function Def() { + return
+
  • +
  • +
  • +
  • +
  • +
    +} \ 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 dadae75..e117154 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRoot.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRoot.jsx @@ -2,6 +2,7 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx"; import {Outlet} from "react-router-dom"; import {CompetitionList} from "./CompetitionList.jsx"; import {CompetitionEdit} from "./CompetitionEdit.jsx"; +import {CompetitionRegisterAdmin} from "./CompetitionRegisterAdmin.jsx"; export function CompetitionRoot() { return <> @@ -21,6 +22,10 @@ export function getCompetitionChildren() { { path: ':id', element: + }, + { + path: ':id/register', + element: } ] } \ No newline at end of file From 743a6911f55c0495ab9b9334f7f2e6aae52775dc Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 12 Feb 2025 14:24:27 +0100 Subject: [PATCH 2/9] feat(insc): add frontend --- .../competition/CompetitionRegisterAdmin.jsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx index cf202c6..74f515a 100644 --- a/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx +++ b/src/main/webapp/src/pages/competition/CompetitionRegisterAdmin.jsx @@ -21,12 +21,19 @@ export function CompetitionRegisterAdmin() { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/competition/${id}/register`, setLoading, 1) + const sortName = (a, b) => { + 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}}) }) + dispatch({type: 'SORT', payload: sortName}) }, [data, clubFilter, catFilter]); const sendRegister = (event, new_state) => { @@ -57,6 +64,7 @@ export function CompetitionRegisterAdmin() { }) } dispatch({type: 'UPDATE_OR_ADD', payload: {id: maxId + 1, data: response.data}}) + dispatch({type: 'SORT', payload: sortName}) document.getElementById("closeModal").click(); }) } @@ -188,7 +196,7 @@ function Modal({sendRegister, modalState, setModalState}) {
    - +
    @@ -243,11 +251,14 @@ function MakeCentralPanel({data, dispatch, id, setModalState}) { {req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"}
    {req.data.fname} {req.data.lname}
    + {req.data.club?.name || "Sans club"}
    - {req.data.club?.name || "Sans club"}
    {req.data.categorie}
    + {req.data.categorie + (req.data.overCategory === 0 ? "" : (" avec " + req.data.overCategory + " de surclassement"))}
    + {req.data.weight ? req.data.weight : "---"} kg +
    From 5d80f209996a919b807f7c3f8f0811791ae3afdb Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Feb 2025 15:38:21 +0100 Subject: [PATCH 8/9] feat(maps): add link to contact --- src/main/webapp/public/club-maps.js | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/public/club-maps.js b/src/main/webapp/public/club-maps.js index bec2e5a..427a37e 100644 --- a/src/main/webapp/public/club-maps.js +++ b/src/main/webapp/public/club-maps.js @@ -1,4 +1,5 @@ -const api_url = "https://intra.ffsaf.fr"; +//const api_url = "https://intra.ffsaf.fr"; +const api_url = "http://localhost:8080"; let map = L.map('map').setView([46.631196, 2.456000], 6); @@ -94,13 +95,45 @@ async function getData() { div_info.appendChild(document.createElement("br")); } else { for (const [key, value] of Object.entries(d.contact)) { - div_info.appendChild(document.createTextNode(`${key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}: ${value}`)); + let str + + if (key === "SITE") { + str = document.createElement('a'); + str.appendChild(document.createTextNode(value)) + str.title = value + str.href = (value.startsWith("http") ? "" : "https://") + value + str.target = '_blank' + } else if (key === "COURRIEL") { + str = document.createElement('a'); + str.appendChild(document.createTextNode(value)) + str.title = value + str.href = `mailto:${value}` + str.target = '_blank' + } else if (key === "INSTAGRAM") { + let r = value.match(/^https?:\/\/www\.instagram\.com\/([a-zA-Z0-9_.\-]+)\/?$/) + str = document.createElement('a'); + str.appendChild(document.createTextNode(r == null ? value : r[1])) + str.title = value + str.href = (value.startsWith("http") ? "" : "https://www.instagram.com/") + value + str.target = '_blank' + } else if (key === "FACEBOOK") { + let r = value.match(/^https?:\/\/www\.facebook\.com\/([a-zA-Z0-9_.\-]+)\/?$/) + str = document.createElement('a'); + str.appendChild(document.createTextNode(r == null ? value : r[1])) + str.title = value + str.href = (value.startsWith("http") ? "" : "https://www.facebook.com/") + value + str.target = '_blank' + } else { + str = document.createTextNode(value) + } + div_info.appendChild(document.createTextNode(`${key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}: `)); + div_info.appendChild(str); div_info.appendChild(document.createElement("br")); } } marker.bindPopup(div_info.innerHTML); - if (icon !== null){ + if (icon !== null) { marker.valueOf()._icon.style.backgroundColor = '#FFFFFF'; marker.valueOf()._icon.style.borderRadius = '10px'; } From 42711cde5de47505d1a9d0ca8c6b572c6667a09d Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Feb 2025 16:03:45 +0100 Subject: [PATCH 9/9] feat(aff): add internal contact field to aff request --- .../ffsaf/data/model/AffiliationRequestModel.java | 1 + .../ffsaf/domain/service/AffiliationService.java | 4 ++++ .../ffsaf/rest/data/SimpleReqAffiliation.java | 3 +++ .../ffsaf/rest/from/AffiliationRequestForm.java | 5 +++++ .../ffsaf/rest/from/AffiliationRequestSaveForm.java | 4 ++++ src/main/webapp/src/pages/DemandeAff.jsx | 10 ++++++++++ .../src/pages/admin/affiliation/AffiliationReqPage.jsx | 3 ++- 7 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java index 9e4ce1b..622d883 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java @@ -24,6 +24,7 @@ public class AffiliationRequestModel { long siret; String RNA; String address; + String contact; String m1_lname; String m1_fname; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 63f1121..87b3e69 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -118,6 +118,7 @@ public class AffiliationService { origine.setName(model.getName()); origine.setRNA(model.getRNA()); origine.setAddress(model.getAddress()); + origine.setContact(model.getContact()); origine.setM1_lname(model.getM1_lname()); origine.setM1_fname(model.getM1_fname()); origine.setM1_lincence(model.getM1_lincence()); @@ -159,6 +160,7 @@ public class AffiliationService { model.setSiret(form.getSiret()); model.setRNA(form.getRna()); model.setAddress(form.getAddress()); + model.setContact(form.getContact()); if (form.getM1_mode() == 2) { model.setM1_lname(form.getM1_lname()); @@ -285,6 +287,7 @@ public class AffiliationService { club.setSIRET(form.getSiret()); club.setRNA(form.getRna()); club.setAddress(form.getAddress()); + club.setContact_intern(form.getContact()); club.setAffiliations(new ArrayList<>()); return Panache.withTransaction(() -> clubRepository.persist(club) .chain(c -> sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation) @@ -321,6 +324,7 @@ public class AffiliationService { club.setSIRET(form.getSiret()); club.setRNA(form.getRna()); club.setAddress(form.getAddress()); + club.setContact_intern(form.getContact()); return Panache.withTransaction(() -> clubRepository.persist(club) .chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison()))) .chain(() -> repositoryRequest.delete(model))); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java index 30d683f..ea0ef71 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java @@ -31,6 +31,8 @@ public class SimpleReqAffiliation { String RNA; @Schema(description = "Adresse de l'association", example = "1 rue de l'exemple, 75000 Paris") String address; + @Schema(description = "Email de contact de l'association", example = "test@test.fr") + String contact; @Schema(description = "Liste des membres pour la demande d'affiliation") List members; @Schema(description = "Saison de l'affiliation", example = "2025") @@ -47,6 +49,7 @@ public class SimpleReqAffiliation { .RNA(model.getRNA()) .address(model.getAddress()) .saison(model.getSaison()) + .contact(model.getContact()) .members(List.of( new AffiliationMember(model.getM1_lname(), model.getM1_fname(), model.getM1_email(), model.getM1_lincence(), model.getM1_role()), diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java index 494acad..961033c 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -33,6 +33,10 @@ public class AffiliationRequestForm { @FormParam("adresse") private String adresse = null; + @Schema(description = "Email de contact de l'association", example = "test@test.fr") + @FormParam("contact") + private String contact = null; + @Schema(description = "La saison de l'affiliation.", example = "2025", required = true) @FormParam("saison") private int saison = -1; @@ -114,6 +118,7 @@ public class AffiliationRequestForm { model.setRNA(this.getRna()); model.setAddress(this.getAdresse()); model.setSaison(this.getSaison()); + model.setContact(this.getContact()); model.setM1_lname(this.getM1_lname()); model.setM1_fname(this.getM1_fname()); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java index ac3012b..409fd45 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java @@ -31,6 +31,10 @@ public class AffiliationRequestSaveForm { @FormParam("address") private String address = null; + @Schema(description = "Email de contact de l'association", example = "test@test.fr") + @FormParam("contact") + private String contact = null; + @Schema(description = "Le statut de l'association.") @FormParam("status") @PartType(MediaType.APPLICATION_OCTET_STREAM) diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index 33d3ea7..151d238 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -208,6 +208,7 @@ function AssoInfo({initData, needFile}) { const [rnaEnable, setRnaEnable] = useState(false) const [adresse, setAdresse] = useState(initData.address ? initData.address : "") const [saison, setSaison] = useState(initData.saison ? initData.saison : getSaison()) + const [contact, setContact] = useState(initData.contact ? initData.contact : "") const fetchSiret = () => { if (siret.length < 14) { @@ -298,6 +299,15 @@ function AssoInfo({initData, needFile}) {
    +
    +
    + Contact administratif* + setContact(e.target.value)}/> +
    +
    +
    diff --git a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx index d931b6b..c245db8 100644 --- a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx +++ b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx @@ -180,6 +180,7 @@ function Content({data, refresh}) { + Status