feat: add membre

This commit is contained in:
Thibaut Valentin 2024-02-28 12:07:24 +01:00
parent 2fd6ef3c2e
commit b0232cd7b7
8 changed files with 310 additions and 47 deletions

View File

@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
@ -9,10 +10,7 @@ import fr.titionfire.ffsaf.net2.request.SReqComb;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.from.ClubMemberForm;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.GroupeUtils;
import fr.titionfire.ffsaf.utils.PageResult;
import fr.titionfire.ffsaf.utils.Pair;
import fr.titionfire.ffsaf.utils.RoleAsso;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
@ -163,10 +161,47 @@ public class MembreService {
.map(__ -> "OK");
}
public Uni<Long> add(FullMemberForm input) {
return clubRepository.findById(input.getClub())
.chain(clubModel -> {
MembreModel model = getMembreModel(input, clubModel);
return Panache.withTransaction(() -> repository.persist(model));
})
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
.map(MembreModel::getId);
}
public Uni<Long> add(FullMemberForm input, String subject) {
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> {
MembreModel model = getMembreModel(input, membreModel.getClub());
model.setRole(RoleAsso.MEMBRE);
model.setGrade_arbitrage(GradeArbitrage.NA);
return Panache.withTransaction(() -> repository.persist(model));
})
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
.map(MembreModel::getId);
}
public Uni<?> setUserId(Long id, String id1) {
return repository.findById(id).chain(membreModel -> {
membreModel.setUserId(id1);
return Panache.withTransaction(() -> repository.persist(membreModel));
});
}
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
MembreModel model = new MembreModel();
model.setFname(input.getFname());
model.setLname(input.getLname());
model.setEmail(input.getEmail());
model.setGenre(input.getGenre());
model.setCountry(input.getCountry());
model.setBirth_date(input.getBirth_date());
model.setCategorie(input.getCategorie());
model.setClub(clubModel);
model.setRole(input.getRole());
model.setGrade_arbitrage(input.getGrade_arbitrage());
return model;
}
}

View File

@ -88,7 +88,7 @@ public class CombEndpoints {
return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel);
}
@POST
@PUT
@Path("{id}")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@ -108,6 +108,22 @@ public class CombEndpoints {
}
@POST
@RolesAllowed({"federation_admin"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<Long> addAdminMembre(FullMemberForm input) {
return membreService.add(input)
.invoke(Unchecked.consumer(id -> {
if (id == null) throw new InternalError("Fail to creat member data");
})).call(id -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data()));
else
return Uni.createFrom().nullItem();
});
}
@PUT
@Path("club/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN)
@ -126,6 +142,23 @@ public class CombEndpoints {
});
}
@POST
@Path("club")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<Long> addMembre(FullMemberForm input) {
return membreService.add(input, idToken.getSubject())
.invoke(Unchecked.consumer(id -> {
if (id == null) throw new InternalError("Fail to creat member data");
})).call(id -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data()));
else
return Uni.createFrom().nullItem();
});
}
private Future<String> replacePhoto(long id, byte[] input) {
return CompletableFuture.supplyAsync(() -> {
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {

View File

@ -3,6 +3,7 @@ import './AdminRoot.css'
import {LoadingProvider} from "../../hooks/useLoading.jsx";
import {MemberList} from "../MemberList.jsx";
import {MemberPage} from "./member/MemberPage.jsx";
import {NewMemberPage} from "./member/NewMemberPage.jsx";
export function AdminRoot() {
return <>
@ -13,7 +14,7 @@ export function AdminRoot() {
</>
}
export function getAdminChildren () {
export function getAdminChildren() {
return [
{
path: 'member',
@ -23,6 +24,10 @@ export function getAdminChildren () {
path: 'member/:id',
element: <MemberPage/>
},
{
path: 'member/new',
element: <NewMemberPage/>
},
{
path: 'b',
element: <div>Admin B</div>

View File

@ -5,6 +5,27 @@ import imageCompression from "browser-image-compression";
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx";
export function addPhoto(event, formData, send) {
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)
}
}
export function InformationForm({data}) {
const setLoading = useLoadingSwitcher()
const handleSubmit = (event) => {
@ -25,7 +46,7 @@ export function InformationForm({data}) {
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
const send = (formData_) => {
apiAxios.post(`/member/${data.id}`, formData_, {
apiAxios.put(`/member/${data.id}`, formData_, {
headers: {
'Accept': '*/*',
'Content-Type': 'multipart/form-data',
@ -40,25 +61,7 @@ export function InformationForm({data}) {
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)
}
addPhoto(event, formData, send);
}
return <form onSubmit={handleSubmit}>

View File

@ -0,0 +1,107 @@
import {useNavigate} from "react-router-dom";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx";
import {addPhoto} from "./InformationForm.jsx";
export function NewMemberPage() {
const navigate = useNavigate();
return <>
<h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
&laquo; retour
</button>
<div>
<div className="row">
<Form/>
</div>
</div>
</>
}
function Form() {
const navigate = useNavigate();
const setLoading = useLoadingSwitcher()
const handleSubmit = (event) => {
event.preventDefault();
setLoading(1)
const formData = new FormData();
formData.append("id", -1);
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`, formData_, {
headers: {
'Accept': '*/*',
'Content-Type': 'multipart/form-data',
}
}).then(data => {
toast.success('Profile crée avec succès 🎉');
navigate(`/admin/member/${data.data}`)
}).catch(e => {
console.log(e.response)
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
}).finally(() => {
if (setLoading)
setLoading(0)
})
}
addPhoto(event, formData, send);
}
return <form onSubmit={handleSubmit}>
<div className="card mb-4">
<div className="card-header">Nouveau membre</div>
<div className="card-body">
<TextField name="lname" text="Nom"/>
<TextField name="fname" text="Prénom"/>
<TextField name="email" text="Email" placeholder="name@example.com"
type="email"/>
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<OptionField name="country" text="Pays" value={'fr'}
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
<BirthDayField/>
<div className="row">
<ClubSelect name="club"/>
</div>
<OptionField name="role" text="Rôle" value={'MEMBRE'}
values={{
MEMBRE: 'Membre',
PRESIDENT: 'Président',
TRESORIER: 'Trésorier',
SECRETAIRE: 'Secrétaire'
}}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={'NA'}
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">Créer</button>
</div>
</div>
</div>
</div>
</form>;
}

View File

@ -3,6 +3,7 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx";
import {MemberPage} from "./member/MemberPage.jsx";
import {useAuth} from "../../hooks/useAuth.jsx";
import {MemberList} from "../MemberList.jsx";
import {NewMemberPage} from "./member/NewMemberPage.jsx";
export function ClubRoot() {
const {userinfo} = useAuth()
@ -35,6 +36,10 @@ export function getClubChildren() {
path: 'member/:id',
element: <MemberPage/>
},
{
path: 'member/new',
element: <NewMemberPage/>
},
{
path: 'b',
element: <div>Club B</div>

View File

@ -3,9 +3,8 @@
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 "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx";
import {addPhoto} from "../../admin/member/InformationForm.jsx";
export function InformationForm({data}) {
const setLoading = useLoadingSwitcher()
@ -25,7 +24,7 @@ export function InformationForm({data}) {
formData.append("role", event.target.role?.value);
const send = (formData_) => {
apiAxios.post(`/member/club/${data.id}`, formData_, {
apiAxios.put(`/member/club/${data.id}`, formData_, {
headers: {
'Accept': '*/*',
'Content-Type': 'multipart/form-data',
@ -40,23 +39,7 @@ export function InformationForm({data}) {
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)
}
addPhoto(event, formData, send);
}
return <form onSubmit={handleSubmit}>
@ -79,7 +62,7 @@ export function InformationForm({data}) {
PRESIDENT: 'Président',
TRESORIER: 'Trésorier',
SECRETAIRE: 'Secrétaire'
}}/>
}} disabled={true}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>
<div className="row">

View File

@ -0,0 +1,92 @@
import {useNavigate} from "react-router-dom";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx";
import {addPhoto} from "../../admin/member/InformationForm.jsx";
export function NewMemberPage() {
const navigate = useNavigate();
return <>
<h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
&laquo; retour
</button>
<div>
<div className="row">
<Form/>
</div>
</div>
</>
}
function Form() {
const navigate = useNavigate();
const setLoading = useLoadingSwitcher()
const handleSubmit = (event) => {
event.preventDefault();
setLoading(1)
const formData = new FormData();
formData.append("id", -1);
formData.append("lname", event.target.lname?.value);
formData.append("fname", event.target.fname?.value);
formData.append("categorie", event.target.category?.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);
const send = (formData_) => {
apiAxios.post(`/member/club`, formData_, {
headers: {
'Accept': '*/*',
'Content-Type': 'multipart/form-data',
}
}).then(data => {
toast.success('Profile crée avec succès 🎉');
navigate(`/club/member/${data.data}`)
}).catch(e => {
console.log(e.response)
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
}).finally(() => {
if (setLoading)
setLoading(0)
})
}
addPhoto(event, formData, send);
}
return <form onSubmit={handleSubmit}>
<div className="card mb-4">
<div className="card-header">Nouveau membre</div>
<div className="card-body">
<TextField name="lname" text="Nom"/>
<TextField name="fname" text="Prénom"/>
<TextField name="email" text="Email" placeholder="name@example.com"
type="email"/>
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<OptionField name="country" text="Pays" value={'fr'}
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
<BirthDayField/>
<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">Créer</button>
</div>
</div>
</div>
</div>
</form>;
}