feat: AffiliationCard
This commit is contained in:
parent
6a21bd4735
commit
b2438ec3d8
@ -21,7 +21,7 @@ public class AffiliationRequestModel {
|
||||
Long id;
|
||||
|
||||
String name;
|
||||
long siren;
|
||||
long siret;
|
||||
String RNA;
|
||||
String address;
|
||||
|
||||
|
||||
@ -53,6 +53,6 @@ public class ClubModel {
|
||||
|
||||
boolean international;
|
||||
|
||||
@OneToMany(mappedBy = "club", fetch = FetchType.EAGER)
|
||||
@OneToMany(mappedBy = "club", fetch = FetchType.LAZY)
|
||||
List<AffiliationModel> affiliations;
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package fr.titionfire.ffsaf.data.repository;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationModel;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AffiliationRepository implements PanacheRepositoryBase<AffiliationModel, Long> {
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationModel;
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.data.repository.AffiliationRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.AffiliationRequestRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.ClubRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleAffiliation;
|
||||
import fr.titionfire.ffsaf.rest.from.AffiliationForm;
|
||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
||||
import fr.titionfire.ffsaf.utils.Utils;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
@ -14,9 +15,10 @@ import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@ -26,7 +28,13 @@ public class AffiliationService {
|
||||
CombRepository combRepository;
|
||||
|
||||
@Inject
|
||||
AffiliationRequestRepository repository;
|
||||
ClubRepository clubRepository;
|
||||
|
||||
@Inject
|
||||
AffiliationRequestRepository repositoryRequest;
|
||||
|
||||
@Inject
|
||||
AffiliationRepository repository;
|
||||
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
@ -57,27 +65,46 @@ public class AffiliationService {
|
||||
throw new IllegalArgumentException("Licence membre n°3 inconnue");
|
||||
}
|
||||
}) : Uni.createFrom().nullItem())
|
||||
).chain(model -> Panache.withTransaction(() -> repository.persist(model)))
|
||||
.onItem().invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
|
||||
).chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
|
||||
"aff_request/logo")))
|
||||
.onItem().invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
|
||||
"aff_request/status")))
|
||||
.map(__ -> "Ok");
|
||||
}
|
||||
|
||||
public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliation() {
|
||||
return Uni.createFrom().nullItem(); // TODO
|
||||
return repository.list("saison = ?1", Utils.getSaison())
|
||||
.map(models -> models.stream().map(SimpleAffiliation::fromModel).toList())
|
||||
.chain(aff -> repositoryRequest.list("saison = ?1", Utils.getSaison())
|
||||
.chain(models -> Uni.join().all(models.stream().map(model ->
|
||||
clubRepository.find("siret = ?1", model.getSiret()).firstResult()
|
||||
.map(c -> new SimpleAffiliation(model.getId() * -1, c.getId(),
|
||||
model.getSaison(), false)))
|
||||
.toList()).andFailFast()
|
||||
).map(aff2 -> Stream.concat(aff2.stream(), aff.stream()).toList())
|
||||
);
|
||||
}
|
||||
|
||||
public Uni<List<SimpleAffiliation>> getAffiliation(long id, Consumer<ClubModel> checkPerm) {
|
||||
return Uni.createFrom().nullItem(); // TODO
|
||||
public Uni<List<SimpleAffiliation>> getAffiliation(long id) {
|
||||
return clubRepository.findById(id).call(model -> Mutiny.fetch(model.getAffiliations()))
|
||||
.chain(model -> repositoryRequest.list("siret = ?1", model.getSIRET())
|
||||
.map(reqs -> reqs.stream().map(req ->
|
||||
new SimpleAffiliation(req.getId() * -1, model.getId(), req.getSaison(), false)))
|
||||
.map(aff2 -> Stream.concat(aff2,
|
||||
model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList())
|
||||
);
|
||||
}
|
||||
|
||||
public Uni<SimpleAffiliation> setAffiliation(long id, AffiliationForm form) {
|
||||
return Uni.createFrom().nullItem(); // TODO
|
||||
public Uni<SimpleAffiliation> setAffiliation(long id, int saison) {
|
||||
return clubRepository.findById(id).chain(club ->
|
||||
Panache.withTransaction(() -> repository.persist(new AffiliationModel(null, club, saison))))
|
||||
.map(SimpleAffiliation::fromModel);
|
||||
}
|
||||
|
||||
public Uni<?> deleteAffiliation(long id) {
|
||||
return Uni.createFrom().nullItem(); // TODO
|
||||
return Panache.withTransaction(() -> repository.deleteById(id));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.domain.service.AffiliationService;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleAffiliation;
|
||||
import fr.titionfire.ffsaf.rest.from.AffiliationForm;
|
||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
||||
import fr.titionfire.ffsaf.utils.GroupeUtils;
|
||||
import io.quarkus.oidc.IdToken;
|
||||
@ -32,8 +30,8 @@ public class AffiliationEndpoints {
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
Consumer<ClubModel> checkPerm = Unchecked.consumer(clubModel -> {
|
||||
if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(clubModel.getId(), idToken))
|
||||
Consumer<Long> checkPerm = Unchecked.consumer(id -> {
|
||||
if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(id, idToken))
|
||||
throw new ForbiddenException();
|
||||
});
|
||||
|
||||
@ -57,16 +55,15 @@ public class AffiliationEndpoints {
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Uni<List<SimpleAffiliation>> getLicence(@PathParam("id") long id) {
|
||||
return service.getAffiliation(id, checkPerm);
|
||||
return Uni.createFrom().item(id).invoke(checkPerm).chain(__ -> service.getAffiliation(id));
|
||||
}
|
||||
|
||||
@POST
|
||||
@PUT
|
||||
@Path("{id}")
|
||||
@RolesAllowed("federation_admin")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Uni<SimpleAffiliation> setLicence(@PathParam("id") long id, AffiliationForm form) {
|
||||
return service.setAffiliation(id, form);
|
||||
public Uni<SimpleAffiliation> setLicence(@PathParam("id") long id, @QueryParam("saison") int saison) {
|
||||
return service.setAffiliation(id, saison);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
|
||||
@ -16,7 +16,7 @@ public class SimpleAffiliation {
|
||||
int saison;
|
||||
boolean validate;
|
||||
|
||||
public static SimpleAffiliation fromModel(AffiliationModel model, boolean validate) {
|
||||
public static SimpleAffiliation fromModel(AffiliationModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
@ -24,7 +24,7 @@ public class SimpleAffiliation {
|
||||
.id(model.getId())
|
||||
.club(model.getClub().getId())
|
||||
.saison(model.getSaison())
|
||||
.validate(validate)
|
||||
.validate(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,8 +14,8 @@ public class AffiliationRequestForm {
|
||||
@FormParam("name")
|
||||
private String name = null;
|
||||
|
||||
@FormParam("siren")
|
||||
private Long siren = null;
|
||||
@FormParam("siret")
|
||||
private Long siret = null;
|
||||
|
||||
@FormParam("rna")
|
||||
private String rna = null;
|
||||
@ -67,7 +67,7 @@ public class AffiliationRequestForm {
|
||||
public AffiliationRequestModel toModel() {
|
||||
AffiliationRequestModel model = new AffiliationRequestModel();
|
||||
model.setName(this.getName());
|
||||
model.setSiren(this.getSiren());
|
||||
model.setSiret(this.getSiret());
|
||||
model.setRNA(this.getRna());
|
||||
model.setAddress(this.getAdresse());
|
||||
|
||||
|
||||
@ -100,14 +100,19 @@ export function DemandeAff() {
|
||||
|
||||
function AssoInfo() {
|
||||
const [denomination, setDenomination] = useState("")
|
||||
const [siren, setSiren] = useState("")
|
||||
const [siret, setSiret] = useState("")
|
||||
const [rna, setRna] = useState("")
|
||||
const [rnaEnable, setRnaEnable] = useState(false)
|
||||
const [adresse, setAdresse] = useState("")
|
||||
|
||||
const fetchSiren = () => {
|
||||
const fetchSiret = () => {
|
||||
if (siret.length < 14) {
|
||||
toast.error("Le SIRET doit contenir 14 chiffres")
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.get(`asso/siren/${siren}`),
|
||||
apiAxios.get(`asso/siren/${siret.substring(0, siret.length - 5)}`),
|
||||
{
|
||||
pending: "Recherche de l'association en cours",
|
||||
success: "Association trouvée avec succès 🎉",
|
||||
@ -131,11 +136,11 @@ function AssoInfo() {
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">N° SIREN*</span>
|
||||
<input type="number" className="form-control" placeholder="siren" name="siren" required value={siren}
|
||||
onChange={e => setSiren(e.target.value)} defaultValue={500213731}/>
|
||||
<span className="input-group-text">N° SIRET*</span>
|
||||
<input type="number" className="form-control" placeholder="siren" name="siren" required value={siret}
|
||||
onChange={e => setSiret(e.target.value)} defaultValue={500213731}/>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
|
||||
onClick={fetchSiren}>Rechercher
|
||||
onClick={fetchSiret}>Rechercher
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -2,51 +2,25 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faEye, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, getSaison} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
function affiliationReducer(affiliation, action) {
|
||||
switch (action.type) {
|
||||
case 'ADD':
|
||||
return [
|
||||
...affiliation,
|
||||
action.payload
|
||||
]
|
||||
case 'REMOVE':
|
||||
return affiliation.filter(affiliation => affiliation.id !== action.payload)
|
||||
case 'UPDATE_OR_ADD':
|
||||
const index = affiliation.findIndex(affiliation => affiliation.id === action.payload.id)
|
||||
if (index === -1) {
|
||||
return [
|
||||
...affiliation,
|
||||
action.payload
|
||||
]
|
||||
} else {
|
||||
affiliation[index] = action.payload
|
||||
return [...affiliation]
|
||||
}
|
||||
case 'SORT':
|
||||
return affiliation.sort((a, b) => b.saison - a.saison)
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
|
||||
|
||||
export function AffiliationCard({clubData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1)
|
||||
|
||||
const [modalAffiliation, setModal] = useState({id: -1, club: clubData.id})
|
||||
const [affiliations, dispatch] = useReducer(affiliationReducer, [])
|
||||
const [affiliations, dispatch] = useReducer(SimpleReducer, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
for (const dataKey of data) {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
|
||||
}
|
||||
dispatch({type: 'SORT'})
|
||||
dispatch({type: 'SORT', payload: (a, b) => b.saison - a.saison})
|
||||
}, [data]);
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
@ -55,7 +29,7 @@ export function AffiliationCard({clubData}) {
|
||||
<div className="col">Affiliation</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#AffiliationModal"
|
||||
onClick={_ => setModal({id: -1, club: clubData.id})}>Ajouter
|
||||
onClick={_ => setModal({id: 0, club: clubData.id, validate: 1})}>Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,7 +66,7 @@ function sendAffiliation(event, dispatch) {
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
toast.promise(
|
||||
apiAxios.post(`/affiliation/${formData.get('membre')}`, formData), // TODO
|
||||
apiAxios.put(`/affiliation/${formData.get('club')}?saison=${formData.get('saison')}`),
|
||||
{
|
||||
pending: "Enregistrement de l'affiliation en cours",
|
||||
success: "Affiliation enregistrée avec succès 🎉",
|
||||
@ -100,12 +74,13 @@ function sendAffiliation(event, dispatch) {
|
||||
}
|
||||
).then(data => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||
dispatch({type: 'SORT'})
|
||||
dispatch({type: 'SORT', payload: (a, b) => b.saison - a.saison})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function removeAffiliation(id, dispatch) {
|
||||
if (id <= 0) return
|
||||
toast.promise(
|
||||
apiAxios.delete(`/affiliation/${id}`),
|
||||
{
|
||||
@ -120,30 +95,21 @@ function removeAffiliation(id, dispatch) {
|
||||
|
||||
function ModalContent({affiliation, dispatch}) {
|
||||
const [saison, setSaison] = useState(0)
|
||||
const [validate, setValidate] = useState(false)
|
||||
const [isNew, setNew] = useState(true)
|
||||
const setSeason = (event) => {
|
||||
setSaison(Number(event.target.value))
|
||||
}
|
||||
const handleValidateChange = (event) => {
|
||||
setValidate(event.target.value === 'true');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (affiliation.id !== -1) {
|
||||
setNew(false)
|
||||
if (affiliation.id !== 0) {
|
||||
setSaison(affiliation.saison)
|
||||
setValidate(affiliation.validate)
|
||||
} else {
|
||||
setNew(true)
|
||||
setSaison(getSaison())
|
||||
setValidate(false)
|
||||
}
|
||||
}, [affiliation]);
|
||||
|
||||
return <form onSubmit={e => sendAffiliation(e, dispatch)}>
|
||||
<input name="id" value={affiliation.id} readOnly hidden/>
|
||||
<input name="membre" value={affiliation.membre} readOnly hidden/>
|
||||
<input name="club" value={affiliation.club} readOnly hidden/>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Edition de l'affiliation</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
@ -151,7 +117,7 @@ function ModalContent({affiliation, dispatch}) {
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
{isNew
|
||||
{affiliation.id === 0
|
||||
? <input type="number" className="form-control" placeholder="Saison" name="saison"
|
||||
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
|
||||
: <><span className="input-group-text" id="basic-addon2">{saison}</span>
|
||||
@ -159,27 +125,20 @@ function ModalContent({affiliation, dispatch}) {
|
||||
<span className="input-group-text" id="basic-addon2">-</span>
|
||||
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
|
||||
</div>
|
||||
<RadioGroupeOnOff name="validate" text="Validation de l'affiliation" value={validate}
|
||||
onChange={handleValidateChange}/>
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<span className="input-group-text" id="basic-addon2">État de la demande</span>
|
||||
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">Validée</span> :
|
||||
<>
|
||||
<span className="input-group-text" id="basic-addon2">En attente</span>
|
||||
<button type="button" className="btn btn-primary"><FontAwesomeIcon icon={faEye}/></button> // TODO
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
{affiliation.id === 0 && <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>}
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
{affiliation.id <= 0 || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeAffiliation(affiliation.id, dispatch)}>Supprimer</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
function RadioGroupeOnOff({value, onChange, name, text}) {
|
||||
return <div className="btn-group input-group mb-3 justify-content-md-center" role="group"
|
||||
aria-label="Basic radio toggle button group">
|
||||
<span className="input-group-text">{text}</span>
|
||||
<input type="radio" className="btn-check" id={"btnradio1" + name} autoComplete="off"
|
||||
value="false" checked={value === false} onChange={onChange}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + name}>Non</label>
|
||||
<input type="radio" className="btn-check" name={name} id={"btnradio2" + name} autoComplete="off"
|
||||
value="true" checked={value === true} onChange={onChange}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + name}>Oui</label>
|
||||
</div>;
|
||||
}
|
||||
@ -52,11 +52,11 @@ export function ClubPage() {
|
||||
<LoadingProvider><AffiliationCard clubData={data}/></LoadingProvider>
|
||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
||||
<button className="btn btn-danger btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#confirm-delete">Supprimer le compte
|
||||
data-bs-target="#confirm-delete">Supprimer le club
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmDialog title="Supprimer le compte"
|
||||
message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
||||
<ConfirmDialog title="Supprimer le club"
|
||||
message="Êtes-vous sûr de vouloir supprimer ce club ?"
|
||||
onConfirm={handleRm}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user