feat: add licence admin gestion
This commit is contained in:
parent
c6130cf65f
commit
0c9020890a
@ -53,6 +53,6 @@ public class MembreModel {
|
|||||||
|
|
||||||
String url_photo;
|
String url_photo;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "membre", fetch = FetchType.EAGER)
|
@OneToMany(mappedBy = "membre", fetch = FetchType.LAZY)
|
||||||
List<LicenceModel> licences;
|
List<LicenceModel> licences;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
package fr.titionfire.ffsaf.data.repository;
|
||||||
|
|
||||||
|
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||||
|
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class LicenceRepository implements PanacheRepositoryBase<LicenceModel, Long> {
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package fr.titionfire.ffsaf.domain.service;
|
||||||
|
|
||||||
|
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||||
|
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||||
|
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
||||||
|
import fr.titionfire.ffsaf.rest.from.LicenceForm;
|
||||||
|
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||||
|
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.hibernate.reactive.mutiny.Mutiny;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@WithSession
|
||||||
|
@ApplicationScoped
|
||||||
|
public class LicenceService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LicenceRepository repository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CombRepository combRepository;
|
||||||
|
|
||||||
|
public Uni<List<LicenceModel>> getLicence(long id) {
|
||||||
|
return combRepository.findById(id).chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
||||||
|
if (form.getId() == -1) {
|
||||||
|
return combRepository.findById(id).chain(combRepository -> {
|
||||||
|
LicenceModel model = new LicenceModel();
|
||||||
|
model.setMembre(combRepository);
|
||||||
|
model.setSaison(form.getSaison());
|
||||||
|
model.setCertificate(form.isCertificate());
|
||||||
|
model.setValidate(form.isValidate());
|
||||||
|
return Panache.withTransaction(() -> repository.persist(model));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return repository.findById(form.getId()).chain(model -> {
|
||||||
|
model.setCertificate(form.isCertificate());
|
||||||
|
model.setValidate(form.isValidate());
|
||||||
|
return Panache.withTransaction(() -> repository.persist(model));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uni<?> deleteLicence(long id) {
|
||||||
|
return Panache.withTransaction(() -> repository.deleteById(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -79,4 +79,5 @@ public class MembreService {
|
|||||||
return Panache.withTransaction(() -> repository.persist(membreModel));
|
return Panache.withTransaction(() -> repository.persist(membreModel));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ public class CombEndpoints {
|
|||||||
return membreService.getById(id).map(SimpleMembre::fromModel);
|
return membreService.getById(id).map(SimpleMembre::fromModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed("federation_admin")
|
||||||
|
|||||||
44
src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java
Normal file
44
src/main/java/fr/titionfire/ffsaf/rest/LicenceEndpoints.java
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package fr.titionfire.ffsaf.rest;
|
||||||
|
|
||||||
|
import fr.titionfire.ffsaf.domain.service.LicenceService;
|
||||||
|
import fr.titionfire.ffsaf.rest.data.SimpleLicence;
|
||||||
|
import fr.titionfire.ffsaf.rest.from.LicenceForm;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Path("api/licence")
|
||||||
|
public class LicenceEndpoints {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LicenceService licenceService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
@RolesAllowed("federation_admin")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Uni<List<SimpleLicence>> getLicence(@PathParam("id") long id) {
|
||||||
|
return licenceService.getLicence(id).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}")
|
||||||
|
@RolesAllowed("federation_admin")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
|
public Uni<SimpleLicence> setLicence(@PathParam("id") long id, LicenceForm form) {
|
||||||
|
return licenceService.setLicence(id, form).map(SimpleLicence::fromModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{id}")
|
||||||
|
@RolesAllowed("federation_admin")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
public Uni<?> deleteLicence(@PathParam("id") long id) {
|
||||||
|
return licenceService.deleteLicence(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package fr.titionfire.ffsaf.rest.data;
|
||||||
|
|
||||||
|
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RegisterForReflection
|
||||||
|
public class SimpleLicence {
|
||||||
|
Long id;
|
||||||
|
Long membre;
|
||||||
|
int saison;
|
||||||
|
boolean certificate;
|
||||||
|
boolean validate;
|
||||||
|
|
||||||
|
public static SimpleLicence fromModel(LicenceModel model) {
|
||||||
|
if (model == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new SimpleLicenceBuilder()
|
||||||
|
.id(model.getId())
|
||||||
|
.membre(model.getMembre().getId())
|
||||||
|
.saison(model.getSaison())
|
||||||
|
.certificate(model.isCertificate())
|
||||||
|
.validate(model.isValidate())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/fr/titionfire/ffsaf/rest/from/LicenceForm.java
Normal file
24
src/main/java/fr/titionfire/ffsaf/rest/from/LicenceForm.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package fr.titionfire.ffsaf.rest.from;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.FormParam;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class LicenceForm {
|
||||||
|
@FormParam("id")
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@FormParam("membre")
|
||||||
|
private long membre;
|
||||||
|
|
||||||
|
@FormParam("saison")
|
||||||
|
private int saison;
|
||||||
|
|
||||||
|
@FormParam("certificate")
|
||||||
|
private boolean certificate;
|
||||||
|
|
||||||
|
@FormParam("validate")
|
||||||
|
private boolean validate;
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import {Outlet} from "react-router-dom";
|
|||||||
import './AdminRoot.css'
|
import './AdminRoot.css'
|
||||||
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||||
import {MemberList} from "./MemberList.jsx";
|
import {MemberList} from "./MemberList.jsx";
|
||||||
import {MemberPage} from "./MemberPage.jsx";
|
import {MemberPage} from "./member/MemberPage.jsx";
|
||||||
|
|
||||||
export function AdminRoot() {
|
export function AdminRoot() {
|
||||||
return <>
|
return <>
|
||||||
|
|||||||
@ -1,455 +0,0 @@
|
|||||||
import {useNavigate, useParams} from "react-router-dom";
|
|
||||||
import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
|
||||||
import {useFetch} from "../../hooks/useFetch.js";
|
|
||||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
|
||||||
import {ClubSelect} from "../../components/ClubSelect.jsx";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
import {apiAxios, getCategoryFormBirthDate} from "../../utils/Tools.js";
|
|
||||||
import imageCompression from "browser-image-compression";
|
|
||||||
import {ColoredCircle} from "../../components/ColoredCircle.jsx";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
|
|
||||||
const vite_url = import.meta.env.VITE_URL;
|
|
||||||
|
|
||||||
export function MemberPage() {
|
|
||||||
const {id} = useParams()
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<h2>Page membre</h2>
|
|
||||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
|
||||||
<< retour
|
|
||||||
</button>
|
|
||||||
{data
|
|
||||||
? <div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-lg-4">
|
|
||||||
<PhotoCard data={data}/>
|
|
||||||
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-8">
|
|
||||||
<InformationForm data={data}/>
|
|
||||||
<LoadingProvider><PremForm userData={data}/></LoadingProvider>
|
|
||||||
<div className="row">
|
|
||||||
<LicenceCard/>
|
|
||||||
<SelectCard/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: error && <AxiosError error={error}/>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function PhotoCard({data}) {
|
|
||||||
return <div className="card mb-4">
|
|
||||||
<div className="card-header">Licence n°{data.licence}</div>
|
|
||||||
<div className="card-body text-center">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<img
|
|
||||||
src={`${vite_url}/api/member/${data.id}/photo`}
|
|
||||||
alt="avatar"
|
|
||||||
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InformationForm({data}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const handleSubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("id", data.id);
|
|
||||||
formData.append("lname", event.target.lname?.value);
|
|
||||||
formData.append("fname", event.target.fname?.value);
|
|
||||||
formData.append("categorie", event.target.category?.value);
|
|
||||||
formData.append("club", event.target.club?.value);
|
|
||||||
formData.append("genre", event.target.genre?.value);
|
|
||||||
formData.append("country", event.target.country?.value);
|
|
||||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
|
||||||
formData.append("email", event.target.email?.value);
|
|
||||||
formData.append("role", event.target.role?.value);
|
|
||||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
|
||||||
|
|
||||||
const send = (formData_) => {
|
|
||||||
apiAxios.post(`/member/${data.id}`, formData_, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
}
|
|
||||||
}).then(_ => {
|
|
||||||
toast.success('Profile mis à jours avec succès 🎉');
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageFile = event.target.url_photo.files[0];
|
|
||||||
if (imageFile) {
|
|
||||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
maxSizeMB: 1,
|
|
||||||
maxWidthOrHeight: 1920,
|
|
||||||
useWebWorker: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
imageCompression(imageFile, options).then(compressedFile => {
|
|
||||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
|
||||||
formData.append("photo_data", compressedFile)
|
|
||||||
send(formData)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
send(formData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmit}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Information</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<TextField name="lname" text="Nom" value={data.lname}/>
|
|
||||||
<TextField name="fname" text="Prénom" value={data.fname}/>
|
|
||||||
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
|
||||||
type="email"/>
|
|
||||||
<OptionField name="genre" text="Genre" value={data.genre}
|
|
||||||
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
|
||||||
<OptionField name="country" text="Pays" value={data.country}
|
|
||||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
|
||||||
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
|
||||||
inti_category={data.categorie}/>
|
|
||||||
<div className="row">
|
|
||||||
<ClubSelect defaultValue={data?.club?.id} name="club"/>
|
|
||||||
</div>
|
|
||||||
<OptionField name="role" text="Rôle" value={data.role}
|
|
||||||
values={{
|
|
||||||
MEMBRE: 'Membre',
|
|
||||||
PRESIDENT: 'Président',
|
|
||||||
TRESORIER: 'Trésorier',
|
|
||||||
SECRETAIRE: 'Secrétaire'
|
|
||||||
}}/>
|
|
||||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
|
||||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
|
||||||
(optionnelle)</label>
|
|
||||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
|
||||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12 text-right">
|
|
||||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PremForm({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const handleSubmitPerm = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("federation_admin", event.target.federation_admin?.checked);
|
|
||||||
formData.append("safca_user", event.target.safca_user?.checked);
|
|
||||||
formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
|
|
||||||
formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
|
|
||||||
|
|
||||||
apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'form-data',
|
|
||||||
}
|
|
||||||
}).then(_ => {
|
|
||||||
toast.success('Permission mise à jours avec succès 🎉');
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmitPerm}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Permission</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="row g-3">
|
|
||||||
{userData.userId
|
|
||||||
? <PremFormContent userData={userData}/>
|
|
||||||
: <div className="col">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Ce membre ne dispose pas de compte...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12 text-right">
|
|
||||||
{userData.userId && <button type="submit" className="btn btn-primary">Enregistrer</button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
function PremFormContent({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="col">
|
|
||||||
<h5>FFSAF intra</h5>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<CheckField name="federation_admin" text="Accès à l'intra"
|
|
||||||
value={data.includes("federation_admin")}/>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>}
|
|
||||||
</div>
|
|
||||||
<div className="col">
|
|
||||||
<h5>SAFCA</h5>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<CheckField name="safca_user" text="Accès à l'application" value={data.includes("safca_user")}/>
|
|
||||||
<CheckField name="safca_create_compet" text="Créer des compétion"
|
|
||||||
value={data.includes("safca_create_compet")}/>
|
|
||||||
<CheckField name="safca_super_admin" text="Super administrateur"
|
|
||||||
value={data.includes("safca_super_admin")}/>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function LicenceCard() {
|
|
||||||
return <div className="col-md-6">
|
|
||||||
<div className="card mb-4 mb-md-0">
|
|
||||||
<div className="card-header">Licence</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<p className="mb-1">Web Design</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectCard() {
|
|
||||||
return <div className="col-md-6">
|
|
||||||
<div className="card mb-4 mb-md-0">
|
|
||||||
<div className="card-header">Sélection en équipe de France</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<p className="mb-1">Web Design</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CompteInfo({userData}) {
|
|
||||||
|
|
||||||
const creatAccount = () => {
|
|
||||||
let err = {};
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
|
|
||||||
err = e
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
pending: 'Création du compte en cours',
|
|
||||||
success: 'Compte créé avec succès 🎉',
|
|
||||||
error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const sendId = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`),
|
|
||||||
{
|
|
||||||
pending: "Définition de l'identifient en cours",
|
|
||||||
success: "Identifient défini avec succès 🎉",
|
|
||||||
error: "Échec de la définition de l'identifient 😕 "
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="card mb-4">
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="btn-group dropend">
|
|
||||||
<div className="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
Compte
|
|
||||||
</div>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li><button type="button" className="btn btn-primary" data-bs-toggle="modal"
|
|
||||||
data-bs-target="#comptIdModal">Définir l'id du compte</button></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body text-center">
|
|
||||||
{userData.userId
|
|
||||||
? <CompteInfoContent userData={userData}/>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Ce membre ne dispose pas de compte...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="modal fade" id="comptIdModal" tabIndex="-1" aria-labelledby="comptIdModalLabel" aria-hidden="true">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<form onSubmit={sendId}>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h1 className="modal-title fs-5" id="comptIdModalLabel">Entré l'UUID du compte</h1>
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<h5>Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...</h5>
|
|
||||||
<input type="text" className="form-control" placeholder="uuid" name="uuid"
|
|
||||||
defaultValue={userData.userId}/>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
||||||
<button type="submit" className="btn btn-primary">Appliquer</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function CompteInfoContent({
|
|
||||||
userData
|
|
||||||
}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Identifiant: {data.login}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>
|
|
||||||
} </>
|
|
||||||
}
|
|
||||||
|
|
||||||
function BirthDayField({inti_date, inti_category}) {
|
|
||||||
const [date, setDate] = useState(inti_date)
|
|
||||||
const [category, setCategory] = useState(inti_category)
|
|
||||||
const [canUpdate, setCanUpdate] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
|
|
||||||
if (b !== canUpdate)
|
|
||||||
setCanUpdate(b)
|
|
||||||
}, [date, category])
|
|
||||||
|
|
||||||
const updateCat = _ => {
|
|
||||||
setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="birth_date">Date de naissance</span>
|
|
||||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
|
|
||||||
name="birth_date" aria-describedby="birth_date" defaultValue={date} required
|
|
||||||
onChange={(e) => setDate(e.target.value)}/>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="category">Catégorie</span>
|
|
||||||
<input type="text" className="form-control" placeholder="" name="category"
|
|
||||||
aria-label="category" value={category} aria-describedby="category"
|
|
||||||
disabled/>
|
|
||||||
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
|
||||||
onClick={updateCat}>Mettre à jours</button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function OptionField({name, text, values, value}) {
|
|
||||||
return <div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" id={name}>{text}</label>
|
|
||||||
<select className="form-select" id={name} name={name} defaultValue={value} required>
|
|
||||||
{Object.keys(values).map((key, _) => {
|
|
||||||
return (<option key={key} value={key}>{values[key]}</option>)
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function TextField({name, text, value, placeholder, type = "text"}) {
|
|
||||||
return <div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id={name}>{text}</span>
|
|
||||||
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
|
|
||||||
name={name} aria-describedby={name} defaultValue={value} required/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function CheckField({name, text, value, row = false}) {
|
|
||||||
return <>{
|
|
||||||
row ?
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div className="form-check">
|
|
||||||
<input className="form-check-input" type="checkbox" id={name} name={name}
|
|
||||||
defaultChecked={value}/>
|
|
||||||
<label className="form-check-label" htmlFor={name}>{text}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: <div className="form-check">
|
|
||||||
<input className="form-check-input" type="checkbox" id={name} name={name} defaultChecked={value}/>
|
|
||||||
<label className="form-check-label" htmlFor={name}>{text}</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
120
src/main/webapp/src/pages/admin/member/CompteInfo.jsx
Normal file
120
src/main/webapp/src/pages/admin/member/CompteInfo.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {apiAxios} from "../../../utils/Tools.js";
|
||||||
|
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||||
|
import {useFetch} from "../../../hooks/useFetch.js";
|
||||||
|
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
|
||||||
|
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||||
|
|
||||||
|
export function CompteInfo({userData}) {
|
||||||
|
|
||||||
|
const creatAccount = () => {
|
||||||
|
let err = {};
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
|
||||||
|
err = e
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
pending: 'Création du compte en cours',
|
||||||
|
success: 'Compte créé avec succès 🎉',
|
||||||
|
error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const sendId = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`),
|
||||||
|
{
|
||||||
|
pending: "Définition de l'identifient en cours",
|
||||||
|
success: "Identifient défini avec succès 🎉",
|
||||||
|
error: "Échec de la définition de l'identifient 😕 "
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="card mb-4">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="btn-group dropend">
|
||||||
|
<div className="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Compte
|
||||||
|
</div>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<button type="button" className="btn btn-primary" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#comptIdModal">Définir l'id du compte
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body text-center">
|
||||||
|
{userData.userId
|
||||||
|
? <CompteInfoContent userData={userData}/>
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Ce membre ne dispose pas de compte...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="modal fade" id="comptIdModal" tabIndex="-1" aria-labelledby="comptIdModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div className="modal-dialog">
|
||||||
|
<div className="modal-content">
|
||||||
|
<form onSubmit={sendId}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h1 className="modal-title fs-5" id="comptIdModalLabel">Entré l'UUID du compte</h1>
|
||||||
|
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<h5>Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...</h5>
|
||||||
|
<input type="text" className="form-control" placeholder="uuid" name="uuid"
|
||||||
|
defaultValue={userData.userId}/>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Appliquer</button>
|
||||||
|
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompteInfoContent({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Identifiant: {data.login}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>
|
||||||
|
} </>
|
||||||
|
}
|
||||||
106
src/main/webapp/src/pages/admin/member/InformationForm.jsx
Normal file
106
src/main/webapp/src/pages/admin/member/InformationForm.jsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||||
|
import {apiAxios} from "../../../utils/Tools.js";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import imageCompression from "browser-image-compression";
|
||||||
|
import {BirthDayField, OptionField, TextField} from "./MemberCustomFiels.jsx";
|
||||||
|
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||||
|
|
||||||
|
export function InformationForm({data}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const handleSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(1)
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("id", data.id);
|
||||||
|
formData.append("lname", event.target.lname?.value);
|
||||||
|
formData.append("fname", event.target.fname?.value);
|
||||||
|
formData.append("categorie", event.target.category?.value);
|
||||||
|
formData.append("club", event.target.club?.value);
|
||||||
|
formData.append("genre", event.target.genre?.value);
|
||||||
|
formData.append("country", event.target.country?.value);
|
||||||
|
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
||||||
|
formData.append("email", event.target.email?.value);
|
||||||
|
formData.append("role", event.target.role?.value);
|
||||||
|
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||||
|
|
||||||
|
const send = (formData_) => {
|
||||||
|
apiAxios.post(`/member/${data.id}`, formData_, {
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}
|
||||||
|
}).then(_ => {
|
||||||
|
toast.success('Profile mis à jours avec succès 🎉');
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e.response)
|
||||||
|
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
|
||||||
|
}).finally(() => {
|
||||||
|
if (setLoading)
|
||||||
|
setLoading(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageFile = event.target.url_photo.files[0];
|
||||||
|
if (imageFile) {
|
||||||
|
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
maxSizeMB: 1,
|
||||||
|
maxWidthOrHeight: 1920,
|
||||||
|
useWebWorker: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCompression(imageFile, options).then(compressedFile => {
|
||||||
|
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||||
|
formData.append("photo_data", compressedFile)
|
||||||
|
send(formData)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
send(formData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={handleSubmit}>
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">Information</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<TextField name="lname" text="Nom" value={data.lname}/>
|
||||||
|
<TextField name="fname" text="Prénom" value={data.fname}/>
|
||||||
|
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
||||||
|
type="email"/>
|
||||||
|
<OptionField name="genre" text="Genre" value={data.genre}
|
||||||
|
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||||
|
<OptionField name="country" text="Pays" value={data.country}
|
||||||
|
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
||||||
|
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
||||||
|
inti_category={data.categorie}/>
|
||||||
|
<div className="row">
|
||||||
|
<ClubSelect defaultValue={data?.club?.id} name="club"/>
|
||||||
|
</div>
|
||||||
|
<OptionField name="role" text="Rôle" value={data.role}
|
||||||
|
values={{
|
||||||
|
MEMBRE: 'Membre',
|
||||||
|
PRESIDENT: 'Président',
|
||||||
|
TRESORIER: 'Trésorier',
|
||||||
|
SECRETAIRE: 'Secrétaire'
|
||||||
|
}}/>
|
||||||
|
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||||
|
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||||
|
(optionnelle)</label>
|
||||||
|
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
||||||
|
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>;
|
||||||
|
}
|
||||||
196
src/main/webapp/src/pages/admin/member/LicenceCard.jsx
Normal file
196
src/main/webapp/src/pages/admin/member/LicenceCard.jsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
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 {AxiosError} from "../../../components/AxiosError.jsx";
|
||||||
|
import {CheckField, TextField} from "./MemberCustomFiels.jsx";
|
||||||
|
import {apiAxios, getSaison} from "../../../utils/Tools.js";
|
||||||
|
import {Input} from "../../../components/Input.jsx";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
|
function licenceReducer(licences, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD':
|
||||||
|
return [
|
||||||
|
...licences,
|
||||||
|
action.payload
|
||||||
|
]
|
||||||
|
case 'REMOVE':
|
||||||
|
return licences.filter(licence => licence.id !== action.payload)
|
||||||
|
case 'UPDATE_OR_ADD':
|
||||||
|
const index = licences.findIndex(licence => licence.id === action.payload.id)
|
||||||
|
if (index === -1) {
|
||||||
|
return [
|
||||||
|
...licences,
|
||||||
|
action.payload
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
licences[index] = action.payload
|
||||||
|
return [...licences]
|
||||||
|
}
|
||||||
|
case 'SORT':
|
||||||
|
return licences.sort((a, b) => b.saison - a.saison)
|
||||||
|
default:
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LicenceCard({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
|
||||||
|
|
||||||
|
const [modalLicence, setModal] = useState({id: -1, membre: userData.id})
|
||||||
|
const [licences, dispatch] = useReducer(licenceReducer, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return
|
||||||
|
for (const dataKey of data) {
|
||||||
|
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
|
||||||
|
}
|
||||||
|
dispatch({type: 'SORT'})
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return <div className="card mb-4 mb-md-0">
|
||||||
|
<div className="card-header container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col">Licence</div>
|
||||||
|
<div className="col" style={{textAlign: 'right'}}>
|
||||||
|
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
||||||
|
onClick={_ => setModal({id: -1, membre: userData.id})}>Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<ul className="list-group">
|
||||||
|
{licences.map((licence, index) => {
|
||||||
|
return <div key={index}
|
||||||
|
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
|
||||||
|
(licence.validate ? "success" : (licence.certificate ? "warning" : "danger"))}>
|
||||||
|
<div className="me-auto">{licence?.saison}-{licence?.saison + 1}</div>
|
||||||
|
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#LicenceModal" onClick={_ => setModal(licence)}>
|
||||||
|
<FontAwesomeIcon icon={faPen}/></button>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
{error && <AxiosError error={error}/>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal fade" id="LicenceModal" tabIndex="-1" aria-labelledby="LicenceModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div className="modal-dialog">
|
||||||
|
<div className="modal-content">
|
||||||
|
<ModalContent licence={modalLicence} dispatch={dispatch}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLicence(event, dispatch) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.post(`/licence/${formData.get('membre')}`, formData),
|
||||||
|
{
|
||||||
|
pending: "Enregistrement de la licence en cours",
|
||||||
|
success: "Licence enregistrée avec succès 🎉",
|
||||||
|
error: "Échec de l'enregistrement de la licence 😕"
|
||||||
|
}
|
||||||
|
).then(data => {
|
||||||
|
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||||
|
dispatch({type: 'SORT'})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLicence(id, dispatch) {
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.delete(`/licence/${id}`),
|
||||||
|
{
|
||||||
|
pending: "Suppression de la licence en cours",
|
||||||
|
success: "Licence supprimée avec succès 🎉",
|
||||||
|
error: "Échec de la suppression de la licence 😕"
|
||||||
|
}
|
||||||
|
).then(data => {
|
||||||
|
dispatch({type: 'REMOVE', payload: id})
|
||||||
|
})
|
||||||
|
console.log(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalContent({licence, dispatch}) {
|
||||||
|
const [saison, setSaison] = useState(0)
|
||||||
|
const [certificate, setCertificate] = useState(false)
|
||||||
|
const [validate, setValidate] = useState(false)
|
||||||
|
const [isNew, setNew] = useState(true)
|
||||||
|
const setSeason = (event) => {
|
||||||
|
setSaison(Number(event.target.value))
|
||||||
|
}
|
||||||
|
const handleCertificateChange = (event) => {
|
||||||
|
setCertificate(event.target.value === 'true');
|
||||||
|
}
|
||||||
|
const handleValidateChange = (event) => {
|
||||||
|
setValidate(event.target.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (licence.id !== -1) {
|
||||||
|
setNew(false)
|
||||||
|
setSaison(licence.saison)
|
||||||
|
setCertificate(licence.certificate)
|
||||||
|
setValidate(licence.validate)
|
||||||
|
} else {
|
||||||
|
setNew(true)
|
||||||
|
setSaison(getSaison())
|
||||||
|
setCertificate(false)
|
||||||
|
setValidate(false)
|
||||||
|
}
|
||||||
|
}, [licence]);
|
||||||
|
|
||||||
|
return <form onSubmit={e => sendLicence(e, dispatch)}>
|
||||||
|
<input name="id" value={licence.id} readOnly hidden/>
|
||||||
|
<input name="membre" value={licence.membre} readOnly hidden/>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h1 className="modal-title fs-5" id="LicenceModalLabel">Edition de la licence</h1>
|
||||||
|
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="input-group mb-3 justify-content-md-center">
|
||||||
|
{isNew
|
||||||
|
? <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>
|
||||||
|
<input name="saison" value={saison} readOnly hidden/></>}
|
||||||
|
<span className="input-group-text" id="basic-addon2">-</span>
|
||||||
|
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
|
||||||
|
</div>
|
||||||
|
<RadioGroupeOnOff name="certificate" text="Certificat médical valide" value={certificate}
|
||||||
|
onChange={handleCertificateChange}/>
|
||||||
|
<RadioGroupeOnOff name="validate" text="Validation de la licence" value={validate}
|
||||||
|
onChange={handleValidateChange}/>
|
||||||
|
</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"
|
||||||
|
onClick={() => removeLicence(licence.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>;
|
||||||
|
}
|
||||||
80
src/main/webapp/src/pages/admin/member/MemberCustomFiels.jsx
Normal file
80
src/main/webapp/src/pages/admin/member/MemberCustomFiels.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {getCategoryFormBirthDate} from "../../../utils/Tools.js";
|
||||||
|
|
||||||
|
export function BirthDayField({inti_date, inti_category}) {
|
||||||
|
const [date, setDate] = useState(inti_date)
|
||||||
|
const [category, setCategory] = useState(inti_category)
|
||||||
|
const [canUpdate, setCanUpdate] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
|
||||||
|
if (b !== canUpdate)
|
||||||
|
setCanUpdate(b)
|
||||||
|
}, [date, category])
|
||||||
|
|
||||||
|
const updateCat = _ => {
|
||||||
|
setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<span className="input-group-text" id="birth_date">Date de naissance</span>
|
||||||
|
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
|
||||||
|
name="birth_date" aria-describedby="birth_date" defaultValue={date} required
|
||||||
|
onChange={(e) => setDate(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<span className="input-group-text" id="category">Catégorie</span>
|
||||||
|
<input type="text" className="form-control" placeholder="" name="category"
|
||||||
|
aria-label="category" value={category} aria-describedby="category"
|
||||||
|
disabled/>
|
||||||
|
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
||||||
|
onClick={updateCat}>Mettre à jours</button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OptionField({name, text, values, value}) {
|
||||||
|
return <div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<label className="input-group-text" id={name}>{text}</label>
|
||||||
|
<select className="form-select" id={name} name={name} defaultValue={value} required>
|
||||||
|
{Object.keys(values).map((key, _) => {
|
||||||
|
return (<option key={key} value={key}>{values[key]}</option>)
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextField({name, text, value, placeholder, type = "text"}) {
|
||||||
|
return <div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<span className="input-group-text" id={name}>{text}</span>
|
||||||
|
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
|
||||||
|
name={name} aria-describedby={name} defaultValue={value} required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckField({name, text, value, row = false}) {
|
||||||
|
return <>{
|
||||||
|
row ?
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div className="form-check">
|
||||||
|
<input className="form-check-input" type="checkbox" id={name} name={name}
|
||||||
|
defaultChecked={value}/>
|
||||||
|
<label className="form-check-label" htmlFor={name}>{text}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <div className="form-check">
|
||||||
|
<input className="form-check-input" type="checkbox" id={name} name={name} defaultChecked={value}/>
|
||||||
|
<label className="form-check-label" htmlFor={name}>{text}</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
72
src/main/webapp/src/pages/admin/member/MemberPage.jsx
Normal file
72
src/main/webapp/src/pages/admin/member/MemberPage.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import {useNavigate, useParams} from "react-router-dom";
|
||||||
|
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||||
|
import {useFetch} from "../../../hooks/useFetch.js";
|
||||||
|
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||||
|
import {CompteInfo} from "./CompteInfo.jsx";
|
||||||
|
import {PremForm} from "./PremForm.jsx";
|
||||||
|
import {InformationForm} from "./InformationForm.jsx";
|
||||||
|
import {LicenceCard} from "./LicenceCard.jsx";
|
||||||
|
|
||||||
|
const vite_url = import.meta.env.VITE_URL;
|
||||||
|
|
||||||
|
export function MemberPage() {
|
||||||
|
const {id} = useParams()
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<h2>Page membre</h2>
|
||||||
|
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||||
|
<< retour
|
||||||
|
</button>
|
||||||
|
{data
|
||||||
|
? <div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-4">
|
||||||
|
<PhotoCard data={data}/>
|
||||||
|
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-8">
|
||||||
|
<InformationForm data={data}/>
|
||||||
|
<LoadingProvider><PremForm userData={data}/></LoadingProvider>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: error && <AxiosError error={error}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PhotoCard({data}) {
|
||||||
|
return <div className="card mb-4">
|
||||||
|
<div className="card-header">Licence n°{data.licence}</div>
|
||||||
|
<div className="card-body text-center">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<img
|
||||||
|
src={`${vite_url}/api/member/${data.id}/photo`}
|
||||||
|
alt="avatar"
|
||||||
|
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectCard() {
|
||||||
|
return <div className="card mb-4 mb-md-0">
|
||||||
|
<div className="card-header">Sélection en équipe de France</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p className="mb-1">Web Design</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
87
src/main/webapp/src/pages/admin/member/PremForm.jsx
Normal file
87
src/main/webapp/src/pages/admin/member/PremForm.jsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||||
|
import {apiAxios} from "../../../utils/Tools.js";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {useFetch} from "../../../hooks/useFetch.js";
|
||||||
|
import {CheckField} from "./MemberCustomFiels.jsx";
|
||||||
|
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||||
|
|
||||||
|
export function PremForm({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const handleSubmitPerm = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(1)
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("federation_admin", event.target.federation_admin?.checked);
|
||||||
|
formData.append("safca_user", event.target.safca_user?.checked);
|
||||||
|
formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
|
||||||
|
formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
|
||||||
|
|
||||||
|
apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Content-Type': 'form-data',
|
||||||
|
}
|
||||||
|
}).then(_ => {
|
||||||
|
toast.success('Permission mise à jours avec succès 🎉');
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e.response)
|
||||||
|
toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
|
||||||
|
}).finally(() => {
|
||||||
|
if (setLoading)
|
||||||
|
setLoading(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={handleSubmitPerm}>
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">Permission</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row g-3">
|
||||||
|
{userData.userId
|
||||||
|
? <PremFormContent userData={userData}/>
|
||||||
|
: <div className="col">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Ce membre ne dispose pas de compte...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
{userData.userId && <button type="submit" className="btn btn-primary">Enregistrer</button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PremFormContent({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="col">
|
||||||
|
<h5>FFSAF intra</h5>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<CheckField name="federation_admin" text="Accès à l'intra"
|
||||||
|
value={data.includes("federation_admin")}/>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>}
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
<h5>SAFCA</h5>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<CheckField name="safca_user" text="Accès à l'application" value={data.includes("safca_user")}/>
|
||||||
|
<CheckField name="safca_create_compet" text="Créer des compétion"
|
||||||
|
value={data.includes("safca_create_compet")}/>
|
||||||
|
<CheckField name="safca_super_admin" text="Super administrateur"
|
||||||
|
value={data.includes("safca_super_admin")}/>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user