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