feat: add privacy setting in me page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m53s

This commit is contained in:
Thibaut Valentin 2026-01-02 14:09:55 +01:00
parent 82231744af
commit 075f01a205
5 changed files with 109 additions and 15 deletions

View File

@ -15,6 +15,7 @@ import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.rest.from.UserSettingForm;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
@ -467,7 +468,8 @@ public class MembreService {
.call(membreModel -> licenceRepository.update("club_id = ?1 where membre = ?2 AND saison = ?3",
(membreModel.getClub() == null) ? null : membreModel.getClub().getId(), membreModel,
Utils.getSaison()))
.call(membreModel -> (membre.getPhoto_data() != null && membre.getPhoto_data().size() > 0) ? ls.logAUpdate("Photo",
.call(membreModel -> (membre.getPhoto_data() != null && membre.getPhoto_data()
.size() > 0) ? ls.logAUpdate("Photo",
membreModel) : Uni.createFrom().nullItem())
.map(__ -> "OK");
}
@ -593,6 +595,20 @@ public class MembreService {
.map(__ -> meData);
}
public Uni<Void> updateSettings(String subject, UserSettingForm settingForm) {
return repository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(membreModel -> {
if (settingForm.getResultPrivacy() != null) {
ls.logChange("Confidentialité des résultats", membreModel.getResultPrivacy(),
settingForm.getResultPrivacy(), membreModel);
membreModel.setResultPrivacy(settingForm.getResultPrivacy());
}
}))
.call(membreModel -> Panache.withTransaction(() -> repository.persist(membreModel)))
.call(membreModel -> ls.append())
.replaceWithVoid();
}
@Scheduled(cron = "0 0 1 1 9 ?")
Uni<Void> everySeason() {
return repository.list("birth_date IS NOT NULL")
@ -602,5 +618,4 @@ public class MembreService {
}).toList()).andCollectFailures())
.map(__ -> null);
}
}

View File

@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.domain.service.PDFService;
import fr.titionfire.ffsaf.rest.data.MeData;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.from.UserSettingForm;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.security.Authenticated;
@ -113,6 +114,21 @@ public class MembreEndpoints {
return pdfService.getLicencePdf(securityCtx.getSubject());
}
@PUT
@Path("me/setting")
@Authenticated
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Met à jour les paramètres du membre connecté", description = "Met à jour les paramètres du membre connecté")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les paramètres ont été mis à jour"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Void> updateMeSettings(UserSettingForm settingForm) {
return membreService.updateSettings(securityCtx.getSubject(), settingForm);
}
@GET
@Path("me/photo")
@Authenticated

View File

@ -1,6 +1,7 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -15,30 +16,32 @@ import java.util.List;
@NoArgsConstructor
@RegisterForReflection
public class MeData {
@Schema(description = "L'identifiant du membre.", example = "1")
@Schema(description = "L'identifiant du membre.", examples = "1")
private long id;
@Schema(description = "Le nom du membre.", example = "Dupont")
@Schema(description = "Le nom du membre.", examples = "Dupont")
private String lname = "";
@Schema(description = "Le prénom du membre.", example = "Jean")
@Schema(description = "Le prénom du membre.", examples = "Jean")
private String fname = "";
@Schema(description = "La catégorie du membre.", example = "SENIOR")
@Schema(description = "La catégorie du membre.", examples = "SENIOR")
private String categorie;
@Schema(description = "Le nom du club du membre.", example = "Association sportive")
@Schema(description = "Le nom du club du membre.", examples = "Association sportive")
private String club;
@Schema(description = "Le genre du membre.", example = "Homme")
@Schema(description = "Le genre du membre.", examples = "Homme")
private String genre;
@Schema(description = "Le numéro de licence du membre.", example = "12345")
@Schema(description = "Le numéro de licence du membre.", examples = "12345")
private int licence;
@Schema(description = "Le pays du membre.", example = "FR")
@Schema(description = "Le pays du membre.", examples = "FR")
private String country;
@Schema(description = "La date de naissance du membre.")
private Date birth_date;
@Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com")
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@examples.com")
private String email;
@Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE")
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
private String role;
@Schema(description = "Le grade d'arbitrage du membre.", example = "N/A")
@Schema(description = "Le grade d'arbitrage du membre.", examples = "N/A")
private String grade_arbitrage;
@Schema(description = "La confidentialité des résultats", examples = "PUBLIC")
private ResultPrivacy resultPrivacy;
@Schema(description = "La liste des licences du membre.")
private List<SimpleLicence> licences;
@ -55,5 +58,6 @@ public class MeData {
this.email = membreModel.getEmail();
this.role = membreModel.getRole().str;
this.grade_arbitrage = membreModel.getGrade_arbitrage().str;
this.resultPrivacy = membreModel.getResultPrivacy();
}
}

View File

@ -0,0 +1,15 @@
package fr.titionfire.ffsaf.rest.from;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import jakarta.ws.rs.FormParam;
import lombok.Getter;
import lombok.ToString;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@ToString
@Getter
public class UserSettingForm {
@FormParam("resultPrivacy")
@Schema(description = "La confidentialité des résultats", examples = "PUBLIC", defaultValue = "PUBLIC", required = true)
private ResultPrivacy resultPrivacy;
}

View File

@ -12,6 +12,10 @@ import {
faUserGroup,
faVenus
} from "@fortawesome/free-solid-svg-icons";
import {CheckField} from "../components/MemberCustomFiels.jsx";
import {toast} from "react-toastify";
import {apiAxios} from "../utils/Tools.js";
import {useEffect, useState} from "react";
const vite_url = import.meta.env.VITE_URL;
@ -30,6 +34,7 @@ export function MePage() {
</div>
<div className="col-lg-8">
<InformationForm data={data}/>
<SettingsCard data={data}/>
<div className="row">
<div className="col-md-6">
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
@ -96,8 +101,47 @@ function SelectCard() {
</div>;
}
function SettingsCard({data}) {
const [privacy, setPrivacy] = useState("PUBLIC");
export function InformationForm({data}) {
useEffect(() => {
if (data?.resultPrivacy) {
setPrivacy(data.resultPrivacy);
}
}, [data]);
const handleChange = (e) => {
const formData = new FormData();
formData.append("resultPrivacy", e.target.value);
toast.promise(apiAxios.put(`/member/me/setting`, formData),
{
pending: 'Mise à jours des paramètres en cours...',
success: 'Paramètres mis à jours avec succès 🎉',
error: 'Échec de la mise à jours des paramètres 😕'
})
.then(() => {
setPrivacy(String(formData.get("resultPrivacy")));
});
}
return <div className="card mb-4">
<div className="card-header">Paramètres du compte</div>
<div className="card-body">
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="email_notifications">Visibilité des résultats</label>
<select className="form-select" id="result_visibility" name="result_visibility" required value={privacy} onChange={handleChange}>
<option value="PUBLIC">Public (visible par tous)</option>
<option value="REGISTERED_ONLY">Membres connectés (visibles par les membres de la fédération)</option>
<option value="REGISTERED_ONLY_NO_DETAILS">Membres connectés - masquer les détails (visibles par les membres de la fédération)
</option>
<option value="PRIVATE">Privé (visible uniquement par moi)</option>
</select>
</div>
</div>
</div>;
}
function InformationForm({data}) {
const style = {marginRight: '0.7em'}
return <div className="card mb-4">
@ -127,4 +171,4 @@ export function InformationForm({data}) {
</div>
</div>
</div>;
}
}