Compare commits

..

32 Commits

Author SHA1 Message Date
c7c7b3ba20 feat: competition result 2025-11-19 16:47:14 +01:00
f8976deb91 feat: re-add siret/rna api 2025-11-19 16:47:14 +01:00
eb9badb4a1 fix: error on loading logo on maps 2025-11-19 16:47:14 +01:00
661fcdb16b fix: remove siret/rna api 2025-11-19 16:47:14 +01:00
b2ad633b21 feat: membre list filter history 2025-11-19 16:47:13 +01:00
1b5bf8ba6c feat: add categorie filter membre search 2025-11-19 16:47:13 +01:00
bf75d9d036 feat: make user-friendly cat name 2025-11-19 16:47:13 +01:00
9457c5749a feat: add ordering for member page 2025-11-19 16:47:13 +01:00
c7f56881cd feat: upgrade club groupe name in kc when club is rename 2025-11-19 16:47:13 +01:00
aebcd62aa9 feat: upgrade aff find to user all id (Rna, siret, siren) 2025-11-19 16:47:13 +01:00
c2eecf4906 fix: certifDate NaN all membre export 2025-11-19 16:47:13 +01:00
87b3bc12e0 fix: null licence on import 2025-11-19 16:47:13 +01:00
3d3d63e58c fix: add more log to import 2025-11-19 16:47:13 +01:00
81c115c655 fix: empty mail on import 2025-11-19 16:47:13 +01:00
a7ba1d16a4 feat: remove kc new account mail 2025-11-19 16:47:13 +01:00
645949a2f6 feat: add cache to getAssoInfo 2025-11-19 16:47:13 +01:00
beb40db1b1 feat: merge rna and siret fields 2025-11-19 16:47:13 +01:00
61a4af6ff1 fix: club delete on RegisterModel 2025-11-19 16:47:13 +01:00
d9fc68298c feat: allow empty mail 2025-11-19 16:47:13 +01:00
fccea5bf6a fix: aff renew select length 2025-11-19 16:47:13 +01:00
ee476cd0e2 feat: remove saison selection on aff req 2025-11-19 16:47:13 +01:00
b320d7db37 fix: affiliation ok login msg 2025-11-19 16:47:13 +01:00
80fef98e07 feat: add email tooltip 2025-11-19 16:47:13 +01:00
7e80703c04 feat: add re-login message 2025-11-19 16:47:10 +01:00
cc4a3e4e06 fix: null email on import 2025-11-19 16:46:46 +01:00
9e28356f2c fix: log detail 2025-11-19 16:46:46 +01:00
77d66813c7 fix: log detail 2025-11-19 16:46:46 +01:00
7625da1d4b feat: add aff req log detail
fix: membre import null email filter
2025-11-19 16:46:46 +01:00
4262845074 fix: typo 2025-11-19 16:46:46 +01:00
b84e10de44 feat: keep log 2025-11-19 16:46:46 +01:00
ac6563ac95 fix: club order 2025-11-19 16:46:46 +01:00
baf57c3464 fix: log message length 2025-11-19 16:46:46 +01:00
55 changed files with 819 additions and 351 deletions

View File

@ -76,6 +76,7 @@ jobs:
key: ${{ secrets.SSH_KEY }} key: ${{ secrets.SSH_KEY }}
script: | script: |
cd ${{ secrets.TARGET_DIR }} cd ${{ secrets.TARGET_DIR }}
docker logs ffsaf > "log/ffsaf_logs_$(date +"%Y-%m-%d_%H-%M-%S").log" 2>&1
docker stop ffsaf docker stop ffsaf
docker rm ffsaf docker rm ffsaf
docker compose up --build -d ffsaf docker compose up --build -d ffsaf

View File

@ -21,8 +21,7 @@ public class AffiliationRequestModel {
Long id; Long id;
String name; String name;
long siret; String state_id;
String RNA;
String address; String address;
String contact; String contact;

View File

@ -55,11 +55,8 @@ public class ClubModel implements LoggableModel {
@Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris") @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris")
String address; String address;
@Schema(description = "RNA du club", example = "W123456789") @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234")
String RNA; String StateId;
@Schema(description = "Numéro SIRET du club", example = "12345678901234")
Long SIRET;
@Schema(description = "Numéro d'affiliation du club", example = "12345") @Schema(description = "Numéro d'affiliation du club", example = "12345")
Long no_affiliation; Long no_affiliation;

View File

@ -30,8 +30,10 @@ public class LogModel {
Long target_id; Long target_id;
@Column(columnDefinition = "TEXT")
String target_name; String target_name;
@Column(columnDefinition = "TEXT")
String message; String message;
public enum ActionType { public enum ActionType {

View File

@ -82,4 +82,22 @@ public class MembreModel implements LoggableModel {
public LogModel.ObjectType getObjectType() { public LogModel.ObjectType getObjectType() {
return LogModel.ObjectType.Membre; return LogModel.ObjectType.Membre;
} }
@Override
public String toString() {
return "MembreModel{" +
"id=" + id +
", userId='" + userId + '\'' +
", lname='" + lname + '\'' +
", fname='" + fname + '\'' +
", categorie=" + categorie +
", genre=" + genre +
", licence=" + licence +
", country='" + country + '\'' +
", birth_date=" + birth_date +
", email='" + email + '\'' +
", role=" + role +
", grade_arbitrage=" + grade_arbitrage +
'}';
}
} }

View File

@ -8,6 +8,8 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
@Getter @Getter
@Setter @Setter
@ -38,6 +40,7 @@ public class RegisterModel {
@ManyToOne(fetch = FetchType.EAGER) @ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "club") @JoinColumn(name = "club")
@OnDelete(action = OnDeleteAction.SET_NULL)
ClubModel club = null; ClubModel club = null;
@Column(nullable = false, columnDefinition = "boolean default false") @Column(nullable = false, columnDefinition = "boolean default false")

View File

@ -22,8 +22,7 @@ public class ClubEntity {
private String training_location; private String training_location;
private String training_day_time; private String training_day_time;
private String contact_intern; private String contact_intern;
private String RNA; private String StateId;
private Long SIRET;
private Long no_affiliation; private Long no_affiliation;
private boolean international; private boolean international;
@ -41,8 +40,7 @@ public class ClubEntity {
.training_location(model.getTraining_location()) .training_location(model.getTraining_location())
.training_day_time(model.getTraining_day_time()) .training_day_time(model.getTraining_day_time())
.contact_intern(model.getContact_intern()) .contact_intern(model.getContact_intern())
.RNA(model.getRNA()) .StateId(model.getStateId())
.SIRET(model.getSIRET())
.no_affiliation(model.getNo_affiliation()) .no_affiliation(model.getNo_affiliation())
.international(model.isInternational()) .international(model.isInternational())
.build(); .build();

View File

@ -2,6 +2,8 @@ package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.client.StateIdService;
import fr.titionfire.ffsaf.rest.data.SimpleAffiliation; import fr.titionfire.ffsaf.rest.data.SimpleAffiliation;
import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation; import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
@ -21,15 +23,19 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream; import java.util.stream.Stream;
@WithSession @WithSession
@ApplicationScoped @ApplicationScoped
public class AffiliationService { public class AffiliationService {
private static final Logger LOGGER = Logger.getLogger(AffiliationService.class);
@Inject @Inject
CombRepository combRepository; CombRepository combRepository;
@ -58,6 +64,12 @@ public class AffiliationService {
@Inject @Inject
LoggerService ls; LoggerService ls;
@RestClient
StateIdService stateIdService;
@RestClient
SirenService sirenService;
@ConfigProperty(name = "upload_dir") @ConfigProperty(name = "upload_dir")
String media; String media;
@ -71,6 +83,8 @@ public class AffiliationService {
public Uni<AffiliationRequestModel> pre_save(AffiliationRequestForm form, boolean unique) { public Uni<AffiliationRequestModel> pre_save(AffiliationRequestForm form, boolean unique) {
AffiliationRequestModel affModel = form.toModel(); AffiliationRequestModel affModel = form.toModel();
int currentSaison = Utils.getSaison(); int currentSaison = Utils.getSaison();
List<String> out = new ArrayList<>();
out.add(affModel.getState_id());
return Uni.createFrom().item(affModel) return Uni.createFrom().item(affModel)
.invoke(Unchecked.consumer(model -> { .invoke(Unchecked.consumer(model -> {
@ -78,14 +92,26 @@ public class AffiliationService {
throw new DBadRequestException("Saison non valid"); throw new DBadRequestException("Saison non valid");
} }
})) }))
.chain(() -> repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(), .chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna(
affModel.getSaison())) affModel.getState_id()) : sirenService.get_unite(affModel.getState_id())
.onItem().invoke(Unchecked.consumer(count -> { .chain(stateIdService::getAssoDataFromUnit)).onItem().transform(o -> {
if (count != 0 && unique) { if (o.getRna() != null && !o.getRna().isBlank())
throw new DBadRequestException("Demande d'affiliation déjà existante"); out.add(o.getRna());
} if (o.getSiren() != null && !o.getSiren().isBlank())
})) out.add(o.getSiren());
.chain(() -> clubRepository.find("SIRET = ?1", affModel.getSiret()).firstResult().chain(club -> if (o.getIdentite().getSiret_siege() != null && !o.getIdentite().getSiret_siege().isBlank())
out.add(o.getIdentite().getSiret_siege());
return out;
}).onFailure().recoverWithItem(out)
.chain(a -> repositoryRequest.count("state_id IN ?1 and saison = ?2",
out, affModel.getSaison()))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0 && unique) {
throw new DBadRequestException("Demande d'affiliation déjà existante");
}
}))
)
.chain(() -> clubRepository.find("StateId IN ?1", out).firstResult().chain(club ->
repository.count("club = ?1 and saison = ?2", club, affModel.getSaison()))) repository.count("club = ?1 and saison = ?2", club, affModel.getSaison())))
.onItem().invoke(Unchecked.consumer(count -> { .onItem().invoke(Unchecked.consumer(count -> {
if (count != 0) { if (count != 0) {
@ -122,7 +148,6 @@ public class AffiliationService {
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.chain(origine -> { .chain(origine -> {
origine.setName(model.getName()); origine.setName(model.getName());
origine.setRNA(model.getRNA());
origine.setAddress(model.getAddress()); origine.setAddress(model.getAddress());
origine.setContact(model.getContact()); origine.setContact(model.getContact());
origine.setM1_lname(model.getM1_lname()); origine.setM1_lname(model.getM1_lname());
@ -146,6 +171,9 @@ public class AffiliationService {
} }
public Uni<String> save(AffiliationRequestForm form) { public Uni<String> save(AffiliationRequestForm form) {
LOGGER.debug("Affiliation Request Created");
LOGGER.debug(form.toString());
// noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher // noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher
return pre_save(form, true) return pre_save(form, true)
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model))) .chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
@ -169,12 +197,14 @@ public class AffiliationService {
} }
public Uni<?> saveAdmin(AffiliationRequestSaveForm form) { public Uni<?> saveAdmin(AffiliationRequestSaveForm form) {
LOGGER.debug("Affiliation Request Saved");
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId()) return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.map(model -> { .map(model -> {
model.setName(form.getName()); model.setName(form.getName());
model.setSiret(form.getSiret()); model.setState_id(form.getState_id());
model.setRNA(form.getRna());
model.setAddress(form.getAddress()); model.setAddress(form.getAddress());
model.setContact(form.getContact()); model.setContact(form.getContact());
@ -259,7 +289,9 @@ public class AffiliationService {
}).call(m -> Panache.withTransaction(() -> combRepository.persist(m))); }).call(m -> Panache.withTransaction(() -> combRepository.persist(m)));
} }
}) })
.call(m -> ((m.getUserId() == null) ? keycloakService.initCompte(m.getId()) : .call(m -> ((m.getUserId() == null) ? keycloakService.initCompte(m.getId())
.onFailure().invoke(t -> LOGGER.warnf("Failed to init account: %s", t.getMessage())).onFailure()
.recoverWithNull() :
keycloakService.setClubGroupMembre(m, club).map(__ -> m.getUserId())) keycloakService.setClubGroupMembre(m, club).map(__ -> m.getUserId()))
.call(userId -> keycloakService.setAutoRoleMembre(userId, m.getRole(), m.getGrade_arbitrage())) .call(userId -> keycloakService.setAutoRoleMembre(userId, m.getRole(), m.getGrade_arbitrage()))
.call(userId -> keycloakService.setEmail(userId, m.getEmail()))) .call(userId -> keycloakService.setEmail(userId, m.getEmail())))
@ -267,19 +299,24 @@ public class AffiliationService {
.call(l1 -> l1 != null && l1.stream().anyMatch(l -> l.getSaison() == saison) ? .call(l1 -> l1 != null && l1.stream().anyMatch(l -> l.getSaison() == saison) ?
Uni.createFrom().nullItem() : Uni.createFrom().nullItem() :
Panache.withTransaction(() -> licenceRepository.persist( Panache.withTransaction(() -> licenceRepository.persist(
new LicenceModel(null, m, club.getId(), saison, null, true, false))) new LicenceModel(null, m, club.getId(), saison, null, true, false)))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, m.getObjectName(), .call(licenceModel -> ls.logA(LogModel.ActionType.ADD, m.getObjectName(),
licenceModel)))); licenceModel))));
} }
public Uni<?> accept(AffiliationRequestSaveForm form) { public Uni<?> accept(AffiliationRequestSaveForm form) {
LOGGER.debug("Affiliation Request Accepted");
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId()) return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.chain(req -> .chain(req ->
clubRepository.find("SIRET = ?1", form.getSiret()).firstResult() clubRepository.find("StateId = ?1", form.getState_id()).firstResult()
.chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model)) .chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model))
.call(club -> setMembre(form.new Member(1), club, req.getSaison()) .call(club -> setMembre(form.new Member(1), club, req.getSaison()).onFailure()
.call(__ -> setMembre(form.new Member(2), club, req.getSaison()) .recoverWithNull()
.call(__ -> setMembre(form.new Member(2), club, req.getSaison()).onFailure()
.recoverWithNull()
.call(___ -> setMembre(form.new Member(3), club, req.getSaison())))) .call(___ -> setMembre(form.new Member(3), club, req.getSaison()))))
.onItem() .onItem()
.invoke(model -> Uni.createFrom() .invoke(model -> Uni.createFrom()
@ -298,13 +335,13 @@ public class AffiliationService {
} }
private Uni<ClubModel> acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) { private Uni<ClubModel> acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) {
LOGGER.debug("New Club Accepted");
return Uni.createFrom().nullItem() return Uni.createFrom().nullItem()
.chain(() -> { .chain(() -> {
ClubModel club = new ClubModel(); ClubModel club = new ClubModel();
club.setName(form.getName()); club.setName(form.getName());
club.setCountry("FR"); club.setCountry("FR");
club.setSIRET(form.getSiret()); club.setStateId(form.getState_id());
club.setRNA(form.getRna());
club.setAddress(form.getAddress()); club.setAddress(form.getAddress());
club.setContact_intern(form.getContact()); club.setContact_intern(form.getContact());
club.setAffiliations(new ArrayList<>()); club.setAffiliations(new ArrayList<>());
@ -336,17 +373,24 @@ public class AffiliationService {
} }
private Uni<ClubModel> acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) { private Uni<ClubModel> acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) {
AtomicBoolean nameChange = new AtomicBoolean(false);
LOGGER.debug("Old Club Accepted");
return Uni.createFrom().nullItem() return Uni.createFrom().nullItem()
.chain(() -> { .chain(() -> {
club.setName(form.getName()); if (!form.getName().equals(club.getName())) {
club.setName(form.getName());
nameChange.set(true);
}
club.setCountry("FR"); club.setCountry("FR");
club.setSIRET(form.getSiret()); club.setStateId(form.getState_id());
club.setRNA(form.getRna());
club.setAddress(form.getAddress()); club.setAddress(form.getAddress());
club.setContact_intern(form.getContact()); club.setContact_intern(form.getContact());
return Panache.withTransaction(() -> clubRepository.persist(club) return Panache.withTransaction(() -> clubRepository.persist(club)
.chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison()))) .chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
.chain(() -> repositoryRequest.delete(model))); .chain(() -> repositoryRequest.delete(model)))
.call(() -> nameChange.get() ? keycloakService.updateGroupFromClub(
club) // update group in keycloak
: Uni.createFrom().nullItem());
}) })
.map(__ -> club); .map(__ -> club);
} }
@ -354,7 +398,7 @@ public class AffiliationService {
public Uni<SimpleReqAffiliation> getRequest(long id) { public Uni<SimpleReqAffiliation> getRequest(long id) {
return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel) return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel)
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.call(out -> clubRepository.find("SIRET = ?1", out.getSiret()).firstResult().invoke(c -> { .call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> {
if (c != null) { if (c != null) {
out.setClub(c.getId()); out.setClub(c.getId());
out.setClub_name(c.getName()); out.setClub_name(c.getName());
@ -367,7 +411,7 @@ public class AffiliationService {
public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliation() { public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliation() {
return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison()) return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison())
.map(models -> models.stream() .map(models -> models.stream()
.map(model -> new SimpleAffiliation(model.getId() * -1, model.getSiret(), model.getSaison(), .map(model -> new SimpleAffiliation(model.getId() * -1, model.getState_id(), model.getSaison(),
false)).toList()) false)).toList())
.chain(aff -> repository.list("saison = ?1", Utils.getSaison()) .chain(aff -> repository.list("saison = ?1", Utils.getSaison())
.map(models -> models.stream().map(SimpleAffiliation::fromModel).toList()) .map(models -> models.stream().map(SimpleAffiliation::fromModel).toList())
@ -379,9 +423,9 @@ public class AffiliationService {
return clubRepository.findById(id) return clubRepository.findById(id)
.onItem().ifNull().failWith(new DNotFoundException("Club non trouvé")) .onItem().ifNull().failWith(new DNotFoundException("Club non trouvé"))
.call(model -> Mutiny.fetch(model.getAffiliations())) .call(model -> Mutiny.fetch(model.getAffiliations()))
.chain(model -> repositoryRequest.list("siret = ?1", model.getSIRET()) .chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId())
.map(reqs -> reqs.stream().map(req -> .map(reqs -> reqs.stream().map(req ->
new SimpleAffiliation(req.getId() * -1, model.getId(), req.getSaison(), false))) new SimpleAffiliation(req.getId() * -1, model.getStateId(), req.getSaison(), false)))
.map(aff2 -> Stream.concat(aff2, .map(aff2 -> Stream.concat(aff2,
model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList()) model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList())
); );
@ -411,9 +455,9 @@ public class AffiliationService {
return Panache.withTransaction(() -> repository.deleteById(id)); return Panache.withTransaction(() -> repository.deleteById(id));
} }
public Uni<?> deleteReqAffiliation(long id, String reason) { public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) {
return repositoryRequest.findById(id) return repositoryRequest.findById(id)
.call(aff -> reactiveMailer.send( .call(aff -> federationAdmin ? reactiveMailer.send(
Mail.withText(aff.getM1_email(), Mail.withText(aff.getM1_email(),
"FFSAF - Votre demande d'affiliation a été rejetée.", "FFSAF - Votre demande d'affiliation a été rejetée.",
String.format( String.format(
@ -430,7 +474,7 @@ public class AffiliationService {
""", aff.getName(), reason) """, aff.getName(), reason)
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr") ).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(aff.getM2_email(), aff.getM3_email()) .addTo(aff.getM2_email(), aff.getM3_email())
)) ) : Uni.createFrom().nullItem())
.chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff))) .chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))
.call(__ -> Utils.deleteMedia(id, media, "aff_request/logo")) .call(__ -> Utils.deleteMedia(id, media, "aff_request/logo"))
.call(__ -> Utils.deleteMedia(id, media, "aff_request/status")); .call(__ -> Utils.deleteMedia(id, media, "aff_request/status"));

View File

@ -32,6 +32,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@ -193,12 +194,17 @@ public class ClubService {
} }
public Uni<String> update(long id, FullClubForm input) { public Uni<String> update(long id, FullClubForm input) {
AtomicBoolean nameChange = new AtomicBoolean(false);
return repository.findById(id).call(m -> Mutiny.fetch(m.getContact())) return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()))
.onItem().transformToUni(Unchecked.function(m -> { .onItem().transformToUni(Unchecked.function(m -> {
TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() { TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
}; };
m.setName(input.getName()); if (!input.getName().equals(m.getName())) {
m.setName(input.getName());
nameChange.set(true);
}
m.setCountry(input.getCountry()); m.setCountry(input.getCountry());
m.setInternational(input.isInternational()); m.setInternational(input.isInternational());
@ -211,11 +217,9 @@ public class ClubService {
m.setTraining_day_time(input.getTraining_day_time()); m.setTraining_day_time(input.getTraining_day_time());
ls.logChange("Contact interne", m.getContact_intern(), input.getContact_intern(), m); ls.logChange("Contact interne", m.getContact_intern(), input.getContact_intern(), m);
m.setContact_intern(input.getContact_intern()); m.setContact_intern(input.getContact_intern());
ls.logChange("N° RNA", m.getRNA(), input.getRna(), m); if (input.getState_id() != null && !input.getState_id().isBlank()) {
m.setRNA(input.getRna()); ls.logChange("N° SIRET", m.getClubId(), input.getState_id(), m);
if (input.getSiret() != null && !input.getSiret().isBlank()) { m.setStateId(input.getState_id());
ls.logChange("N° SIRET", m.getSIRET(), input.getSiret(), m);
m.setSIRET(Long.parseLong(input.getSiret()));
} }
ls.logChange("Adresse administrative", m.getAddress(), input.getAddress(), m); ls.logChange("Adresse administrative", m.getAddress(), input.getAddress(), m);
m.setAddress(input.getAddress()); m.setAddress(input.getAddress());
@ -230,6 +234,8 @@ public class ClubService {
} }
return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append()); return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
})) }))
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(clubModel) // update group in keycloak
: Uni.createFrom().nullItem())
.invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients, .invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
SimpleClubModel.fromModel(membreModel))) SimpleClubModel.fromModel(membreModel)))
.map(__ -> "OK"); .map(__ -> "OK");
@ -251,9 +257,8 @@ public class ClubService {
clubModel.setTraining_location(input.getTraining_location()); clubModel.setTraining_location(input.getTraining_location());
clubModel.setTraining_day_time(input.getTraining_day_time()); clubModel.setTraining_day_time(input.getTraining_day_time());
clubModel.setContact_intern(input.getContact_intern()); clubModel.setContact_intern(input.getContact_intern());
clubModel.setRNA(input.getRna()); if (input.getState_id() != null && !input.getState_id().isBlank())
if (input.getSiret() != null && !input.getSiret().isBlank()) clubModel.setStateId(input.getState_id());
clubModel.setSIRET(Long.parseLong(input.getSiret()));
clubModel.setAddress(input.getAddress()); clubModel.setAddress(input.getAddress());
try { try {
@ -300,9 +305,9 @@ public class ClubService {
.call(clubModel -> Mutiny.fetch(clubModel.getAffiliations())) .call(clubModel -> Mutiny.fetch(clubModel.getAffiliations()))
.invoke(clubModel -> { .invoke(clubModel -> {
data.setName(clubModel.getName()); data.setName(clubModel.getName());
data.setSiret(clubModel.getSIRET()); data.setState_id(clubModel.getStateId());
data.setRna(clubModel.getRNA());
data.setAddress(clubModel.getAddress()); data.setAddress(clubModel.getAddress());
data.setContact(clubModel.getContact_intern());
data.setSaison( data.setSaison(
clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison)) clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison))
.map(AffiliationModel::getSaison).map(i -> Math.min(i + 1, Utils.getSaison() + 1)) .map(AffiliationModel::getSaison).map(i -> Math.min(i + 1, Utils.getSaison() + 1))

View File

@ -85,6 +85,31 @@ public class KeycloakService {
return Uni.createFrom().item(club::getClubId); return Uni.createFrom().item(club::getClubId);
} }
public Uni<String> updateGroupFromClub(ClubModel club) {
if (club.getClubId() == null) {
return getGroupFromClub(club);
} else {
LOGGER.infof("Updating name of club group %d-%s...", club.getId(), club.getName());
return vertx.getOrCreateContext().executeBlocking(() -> {
GroupRepresentation clubGroup =
keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
.findAny()
.orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream()
.filter(g -> g.getName().startsWith(club.getId() + "-")).findAny()
.ifPresent(groupRepresentation -> {
groupRepresentation.setName(club.getId() + "-" + club.getName());
keycloak.realm(realm).groups().group(groupRepresentation.getId())
.update(groupRepresentation);
});
return club.getClubId();
}
);
}
}
public Uni<String> getUserFromMember(MembreModel membreModel) { public Uni<String> getUserFromMember(MembreModel membreModel) {
if (membreModel.getUserId() == null) { if (membreModel.getUserId() == null) {
return Uni.createFrom() return Uni.createFrom()
@ -199,16 +224,16 @@ public class KeycloakService {
public Uni<String> initCompte(long id) { public Uni<String> initCompte(long id) {
return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> { return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> {
if (membreModel.getUserId() != null) if (membreModel.getUserId() != null)
throw new KeycloakException("User already linked to the user id=" + id); throw new KeycloakException("User already linked to the user id=" + id);
if (membreModel.getEmail() == null) if (membreModel.getEmail() == null)
throw new KeycloakException("User email is null"); throw new KeycloakException("User email is null");
if (membreModel.getFname() == null || membreModel.getLname() == null) if (membreModel.getFname() == null || membreModel.getLname() == null)
throw new KeycloakException("User name is null"); throw new KeycloakException("User name is null");
})).chain(membreModel -> creatUser(membreModel).chain(user -> { })).chain(membreModel -> creatUser(membreModel).chain(user -> {
LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId()); LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId()); return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId());
})); }));
} }
private Uni<UserRepresentation> creatUser(MembreModel membreModel) { private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
@ -231,9 +256,6 @@ public class KeycloakService {
user.setEmail(membreModel.getEmail()); user.setEmail(membreModel.getEmail());
user.setEnabled(true); user.setEnabled(true);
user.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name(),
RequiredAction.UPDATE_PASSWORD.name()));
try (Response response = keycloak.realm(realm).users().create(user)) { try (Response response = keycloak.realm(realm).users().create(user)) {
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo() if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo()
.equals(Response.Status.CONFLICT)) .equals(Response.Status.CONFLICT))
@ -245,13 +267,6 @@ public class KeycloakService {
return getUser(login).orElseThrow( return getUser(login).orElseThrow(
() -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin))); () -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin)));
}) })
.call(user -> enabled_email ?
vertx.getOrCreateContext().executeBlocking(() -> {
keycloak.realm(realm).users().get(user.getId())
.executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(),
RequiredAction.UPDATE_PASSWORD.name()));
return null;
}) : Uni.createFrom().nullItem())
.invoke(user -> membreModel.setUserId(user.getId())) .invoke(user -> membreModel.setUserId(user.getId()))
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of())) .call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
.call(user -> enabled_email ? reactiveMailer.send( .call(user -> enabled_email ? reactiveMailer.send(
@ -261,14 +276,14 @@ public class KeycloakService {
""" """
Bonjour, Bonjour,
Suite à votre première inscription % la Fédération Française de Soft Armored Fighting (FFSAF), votre compte pour accéder à l'intranet a été créé. Suite à votre première inscription % la Fédération Française de Soft Armored Fighting (FFSAF), votre compte intranet a été créé.
Ce compte vous permettra de consulter vos informations, de vous inscrire aux compétitions et de consulter vos résultats. Ce compte vous permettra de consulter vos informations et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats.
Vous allez recevoir dans les prochaines minutes un email vous demandant de vérifier votre email et de définir un mot de passe.
L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
Votre nom d'utilisateur est : %s Votre nom d'utilisateur est : %s
Pour définir votre mot de passe, rendez-vous sur l'intranet > "Connexion" > "Mot de passe oublié ?"
Si vous n'avez pas demandé cette inscription, veuillez contacter le support à l'adresse support@ffsaf.fr. Si vous n'avez pas demandé cette inscription, veuillez contacter le support à l'adresse support@ffsaf.fr.
(Pas de panique, nous ne vous enverrons pas de message autre que ce concernant votre compte) (Pas de panique, nous ne vous enverrons pas de message autre que ce concernant votre compte)

View File

@ -18,6 +18,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -26,6 +27,7 @@ import java.util.function.Function;
@WithSession @WithSession
@ApplicationScoped @ApplicationScoped
public class LicenceService { public class LicenceService {
private static final Logger LOGGER = Logger.getLogger(LicenceService.class);
@Inject @Inject
LicenceRepository repository; LicenceRepository repository;
@ -125,7 +127,9 @@ public class LicenceService {
.chain(() -> combRepository.persist(membreModel)) .chain(() -> combRepository.persist(membreModel))
: Uni.createFrom().nullItem()) : Uni.createFrom().nullItem())
.call(__ -> (membreModel.getUserId() == null) ? .call(__ -> (membreModel.getUserId() == null) ?
keycloakService.initCompte(membreModel.getId()) keycloakService.initCompte(membreModel.getId()).onFailure()
.invoke(t -> LOGGER.infof("Failed to init account: %s", t.getMessage())).onFailure()
.recoverWithNull()
: Uni.createFrom().nullItem()); : Uni.createFrom().nullItem());
} }

View File

@ -13,6 +13,7 @@ import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData; import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException; 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.FullMemberForm;
import fr.titionfire.ffsaf.utils.*; import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.Panache;
@ -102,15 +103,47 @@ public class MembreService {
return baseUni; return baseUni;
} }
private Sort getSort(String order) {
Sort sort;
if (order == null || order.isBlank()) {
sort = Sort.ascending("fname", "lname");
} else {
sort = Sort.empty();
for (String e : order.split(",")) {
String[] split = e.split(" ");
if (split.length == 2) {
sort = sort.and(split[0],
split[1].equals("n") ? Sort.Direction.Ascending : Sort.Direction.Descending);
} else {
return null;
}
}
}
return sort;
}
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club, public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club,
int licenceRequest, int payState) { int licenceRequest, int payState, String order, String categorie) {
if (search == null) if (search == null)
search = ""; search = "";
search = "%" + search.replaceAll(" ", "% %") + "%"; search = "%" + search.replaceAll(" ", "% %") + "%";
String categorieFilter;
if (categorie == null || categorie.isBlank())
categorieFilter = " True";
else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
String finalSearch = search; String finalSearch = search;
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState); Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
Sort sort = getSort(order);
if (sort == null)
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
return baseUni return baseUni
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> { .chain(ids -> {
@ -120,18 +153,18 @@ public class MembreService {
if (club == null || club.isBlank()) { if (club == null || club.isBlank()) {
query = repository.find( query = repository.find(
"id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")", "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
Sort.ascending("fname", "lname"), finalSearch, ids) sort, finalSearch, ids)
.page(Page.ofSize(limit)); .page(Page.ofSize(limit));
} else { } else {
if (club.equals("null")) { if (club.equals("null")) {
query = repository.find( query = repository.find(
"id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ")", "id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
Sort.ascending("fname", "lname"), finalSearch, ids).page(Page.ofSize(limit)); sort, finalSearch, ids).page(Page.ofSize(limit));
} else { } else {
query = repository.find( query = repository.find(
"id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")", "id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
Sort.ascending("fname", "lname"), finalSearch, club + "%", ids) sort, finalSearch, club, ids)
.page(Page.ofSize(limit)); .page(Page.ofSize(limit));
} }
} }
@ -140,7 +173,7 @@ public class MembreService {
} }
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState, public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
String subject) { String order, String categorie, String subject) {
if (search == null) if (search == null)
search = ""; search = "";
search = "%" + search.replaceAll(" ", "% %") + "%"; search = "%" + search.replaceAll(" ", "% %") + "%";
@ -149,6 +182,16 @@ public class MembreService {
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState); Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
String categorieFilter;
if (categorie == null || categorie.isBlank())
categorieFilter = " True";
else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
Sort sort = getSort(order);
if (sort == null)
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
return baseUni return baseUni
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> { .chain(ids -> {
@ -157,8 +200,8 @@ public class MembreService {
return repository.find("userId = ?1", subject).firstResult() return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> { .chain(membreModel -> {
PanacheQuery<MembreModel> query = repository.find( PanacheQuery<MembreModel> query = repository.find(
"id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")", "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub(), ids) sort, finalSearch, membreModel.getClub(), ids)
.page(Page.ofSize(limit)); .page(Page.ofSize(limit));
return getPageResult(query, limit, page); return getPageResult(query, limit, page);
}); });
@ -197,6 +240,11 @@ public class MembreService {
return Uni.createFrom().nullItem(); return Uni.createFrom().nullItem();
AtomicReference<ClubModel> clubModel = new AtomicReference<>(); AtomicReference<ClubModel> clubModel = new AtomicReference<>();
LOGGER.debugf("Membre import (size=%d)", data2.size());
for (SimpleMembreInOutData simpleMembreInOutData : data2) {
LOGGER.debugf("-> %s", simpleMembreInOutData.toString());
}
return repository.find("userId = ?1", subject).firstResult() return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> { .chain(membreModel -> {
clubModel.set(membreModel.getClub()); clubModel.set(membreModel.getClub());
@ -205,20 +253,24 @@ public class MembreService {
return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3", return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3",
data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(), data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(),
data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(), data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(),
data2.stream().map(SimpleMembreInOutData::getEmail).filter(Objects::nonNull).toList()); data2.stream().map(SimpleMembreInOutData::getEmail).filter(o -> o != null && !o.isBlank())
.toList());
}) })
.call(Unchecked.function(membres -> { .call(Unchecked.function(membres -> {
for (MembreModel membreModel : membres) { for (MembreModel membreModel : membres) {
if (!Objects.equals(membreModel.getClub(), clubModel.get())) if (!Objects.equals(membreModel.getClub(), clubModel.get())) {
LOGGER.info("Similar membres found: " + membreModel);
throw new DForbiddenException( throw new DForbiddenException(
"Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club"); "Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club");
}
} }
Uni<Void> uniResult = Uni.createFrom().voidItem(); Uni<Void> uniResult = Uni.createFrom().voidItem();
for (SimpleMembreInOutData dataIn : data2) { for (SimpleMembreInOutData dataIn : data2) {
MembreModel model = membres.stream() MembreModel model = membres.stream()
.filter(m -> Objects.equals(m.getLicence(), dataIn.getLicence()) || m.getLname() .filter(m -> (dataIn.getLicence() != null && Objects.equals(m.getLicence(),
.equals(dataIn.getNom()) && m.getFname().equals(dataIn.getPrenom()) || dataIn.getLicence())) || m.getLname().equals(dataIn.getNom()) && m.getFname()
Objects.equals(m.getFname(), dataIn.getEmail())).findFirst() .equals(dataIn.getPrenom()) || (dataIn.getEmail() != null && !dataIn.getEmail()
.isBlank() && Objects.equals(m.getFname(), dataIn.getEmail()))).findFirst()
.orElseGet(() -> { .orElseGet(() -> {
MembreModel mm = new MembreModel(); MembreModel mm = new MembreModel();
mm.setClub(clubModel.get()); mm.setClub(clubModel.get());
@ -226,16 +278,23 @@ public class MembreService {
mm.setCountry("FR"); mm.setCountry("FR");
return mm; return mm;
}); });
if (model.getId() != null) {
LOGGER.debugf("updating -> %s", dataIn.toString());
} else {
LOGGER.debugf("creating -> %s", dataIn.toString());
}
if (model.getEmail() != null) { if (model.getEmail() != null && !model.getEmail().isBlank()) {
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
throw new DBadRequestException("Email déja utiliser"); LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser");
} }
if (StringSimilarity.similarity(model.getLname().toUpperCase(), if (StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
throw new DBadRequestException("Email déja utiliser"); LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser");
} }
} }
@ -244,6 +303,7 @@ public class MembreService {
if ((!add && StringSimilarity.similarity(model.getLname().toUpperCase(), if ((!add && StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3) || (!add && StringSimilarity.similarity( dataIn.getNom().toUpperCase()) > 3) || (!add && StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException( throw new DBadRequestException(
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide."); "Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide.");
} }
@ -319,7 +379,7 @@ public class MembreService {
return update(repository.findById(id) return update(repository.findById(id)
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0) if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser"); throw new DBadRequestException("Email déjà utiliser");
}))) })))
.chain(membreModel -> clubRepository.findById(membre.getClub()) .chain(membreModel -> clubRepository.findById(membre.getClub())
@ -341,7 +401,7 @@ public class MembreService {
return update(repository.findById(id) return update(repository.findById(id)
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0) if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser"); throw new DBadRequestException("Email déjà utiliser");
}))) })))
.invoke(Unchecked.consumer(membreModel -> { .invoke(Unchecked.consumer(membreModel -> {

View File

@ -92,7 +92,7 @@ public class AffiliationRequestEndpoints {
@DELETE @DELETE
@Path("/{id}") @Path("/{id}")
@RolesAllowed({"federation_admin"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " + @Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " +
"d'affiliation pour l'identifiant spécifié.") "d'affiliation pour l'identifiant spécifié.")
@ -107,7 +107,7 @@ public class AffiliationRequestEndpoints {
if (o.getClub() == null && !securityCtx.roleHas("federation_admin")) if (o.getClub() == null && !securityCtx.roleHas("federation_admin"))
throw new DForbiddenException(); throw new DForbiddenException();
})).invoke(o -> checkPerm.accept(o.getClub())) })).invoke(o -> checkPerm.accept(o.getClub()))
.chain(o -> service.deleteReqAffiliation(id, reason)); .chain(o -> service.deleteReqAffiliation(id, reason, securityCtx.roleHas("federation_admin")));
} }
@PUT @PUT

View File

@ -1,7 +1,8 @@
package fr.titionfire.ffsaf.rest; package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.rest.client.SirenService; import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot; import fr.titionfire.ffsaf.rest.client.StateIdService;
import fr.titionfire.ffsaf.rest.data.AssoData;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
@ -12,18 +13,24 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("api/asso") @Path("api/asso")
public class AssoEndpoints { public class AssoEndpoints {
@RestClient
StateIdService stateIdService;
@RestClient @RestClient
SirenService sirenService; SirenService sirenService;
@GET @GET
@Path("siren/{siren}") @Path("state_id/{stateId}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true) @Operation(hidden = true)
public Uni<UniteLegaleRoot> getInfoSiren(@PathParam("siren") String siren) { public Uni<AssoData> getAssoInfo(@PathParam("stateId") String stateId) {
return sirenService.get_unite(siren).onFailure().transform(throwable -> { return ((stateId.charAt(0) == 'W') ? stateIdService.get_rna(stateId) : sirenService.get_unite(
stateId).chain(stateIdService::getAssoDataFromUnit)).onFailure().transform(throwable -> {
if (throwable instanceof WebApplicationException exception) { if (throwable instanceof WebApplicationException exception) {
if (exception.getResponse().getStatus() == 404)
return new DNotFoundException("Service momentanément indisponible");
if (exception.getResponse().getStatus() == 400) if (exception.getResponse().getStatus() == 400)
return new DNotFoundException("Siret introuvable"); return new DNotFoundException("Asso introuvable");
} }
return throwable; return throwable;
}); });

View File

@ -29,6 +29,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -69,7 +70,8 @@ public class ClubEndpoints {
@APIResponse(responseCode = "500", description = "Erreur interne du serveur") @APIResponse(responseCode = "500", description = "Erreur interne du serveur")
}) })
public Uni<List<SimpleClubModel>> getAll() { public Uni<List<SimpleClubModel>> getAll() {
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()); return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).sorted(
Comparator.comparing(SimpleClubModel::getName)).toList());
} }
@GET @GET

View File

@ -58,13 +58,15 @@ public class MembreAdminEndpoints {
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
@Parameter(description = "Text à rechercher") @QueryParam("search") String search, @Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@Parameter(description = "Club à filter") @QueryParam("club") String club, @Parameter(description = "Club à filter") @QueryParam("club") String club,
@Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, @Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie,
@Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) { @Parameter(description = "État de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest,
@Parameter(description = "État du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment,
@Parameter(description = "Ordre") @QueryParam("order") String order) {
if (limit == null) if (limit == null)
limit = 50; limit = 50;
if (page == null || page < 1) if (page == null || page < 1)
page = 1; page = 1;
return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment); return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order, categorie);
} }
@GET @GET

View File

@ -50,13 +50,15 @@ public class MembreClubEndpoints {
@Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit, @Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit,
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
@Parameter(description = "Text à rechercher") @QueryParam("search") String search, @Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie,
@Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, @Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest,
@Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) { @Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment,
@Parameter(description = "Ordre") @QueryParam("order") String order) {
if (limit == null) if (limit == null)
limit = 50; limit = 50;
if (page == null || page < 1) if (page == null || page < 1)
page = 1; page = 1;
return membreService.search(limit, page - 1, search, licenceRequest, payment, securityCtx.getSubject()); return membreService.search(limit, page - 1, search, licenceRequest, payment, order, categorie, securityCtx.getSubject());
} }
@GET @GET

View File

@ -1,6 +1,7 @@
package fr.titionfire.ffsaf.rest.client; package fr.titionfire.ffsaf.rest.client;
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot; import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
import io.quarkus.cache.CacheResult;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
@ -15,5 +16,6 @@ public interface SirenService {
@GET @GET
@Path("/v3/unites_legales/{SIREN}") @Path("/v3/unites_legales/{SIREN}")
@CacheResult(cacheName = "AssoData_siren")
Uni<UniteLegaleRoot> get_unite(@PathParam("SIREN") String siren); Uni<UniteLegaleRoot> get_unite(@PathParam("SIREN") String siren);
} }

View File

@ -0,0 +1,48 @@
package fr.titionfire.ffsaf.rest.client;
import fr.titionfire.ffsaf.rest.data.AssoData;
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
import io.quarkus.cache.CacheResult;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@Path("/")
@RegisterRestClient
public interface StateIdService {
@GET
@Path("/associations/{rna}")
@CacheResult(cacheName = "AssoData_rna")
Uni<AssoData> get_rna(@PathParam("rna") String rna);
default Uni<AssoData> getAssoDataFromUnit(UniteLegaleRoot u) {
AssoData assoData = new AssoData();
assoData.setSiren(u.getUnite_legale().getSiren());
assoData.setRna(u.getUnite_legale().getIdentifiant_association());
AssoData.Identite identite = new AssoData.Identite();
identite.setNom(u.getUnite_legale().getDenomination());
identite.setSiret_siege(u.getUnite_legale().getEtablissement_siege().getSiret());
assoData.setIdentite(identite);
AssoData.Address address = new AssoData.Address();
StringBuilder voie = new StringBuilder();
if (u.getUnite_legale().getEtablissement_siege().getNumero_voie() != null)
voie.append(u.getUnite_legale().getEtablissement_siege().getNumero_voie()).append(' ');
if (u.getUnite_legale().getEtablissement_siege().getType_voie() != null)
voie.append(u.getUnite_legale().getEtablissement_siege().getType_voie()).append(' ');
if (u.getUnite_legale().getEtablissement_siege().getLibelle_voie() != null)
voie.append(u.getUnite_legale().getEtablissement_siege().getLibelle_voie()).append(' ');
address.setVoie(voie.toString().trim());
address.setComplement(u.getUnite_legale().getEtablissement_siege().getComplement_adresse());
address.setCode_postal(u.getUnite_legale().getEtablissement_siege().getCode_postal());
address.setCommune(
new AssoData.Commune(u.getUnite_legale().getEtablissement_siege().getLibelle_commune()));
assoData.setCoordonnees(new AssoData.Coordonnee(address));
return Uni.createFrom().item(assoData);
}
}

View File

@ -0,0 +1,48 @@
package fr.titionfire.ffsaf.rest.data;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@RegisterForReflection
public class AssoData {
String siren;
String rna;
Identite identite;
Coordonnee coordonnees;
@Data
@RegisterForReflection
public static class Identite {
String nom;
String siret_siege;
}
@Data
@RegisterForReflection
@NoArgsConstructor
@AllArgsConstructor
public static class Coordonnee {
Address adresse_gestion;
}
@Data
@RegisterForReflection
public static class Address {
String voie;
String complement;
String code_postal;
String pays;
Commune commune;
}
@Data
@RegisterForReflection
@NoArgsConstructor
@AllArgsConstructor
public static class Commune {
String nom;
}
}

View File

@ -17,9 +17,9 @@ import java.util.List;
@RegisterForReflection @RegisterForReflection
public class RenewAffData { public class RenewAffData {
String name; String name;
Long siret; String state_id;
String rna;
String address; String address;
String contact;
int saison; int saison;
List<RenewMember> members; List<RenewMember> members;

View File

@ -14,8 +14,8 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class SimpleAffiliation { public class SimpleAffiliation {
@Schema(description = "L'identifiant de l'affiliation.", example = "1") @Schema(description = "L'identifiant de l'affiliation.", example = "1")
private Long id; private Long id;
@Schema(description = "L'identifiant du club associé à l'affiliation.", example = "123") @Schema(description = "L'identifiant du club associé à l'affiliation si id > 0 sinon n° SIRET ou RNA du club.", example = "123")
private Long club; private String club;
@Schema(description = "La saison de l'affiliation.", example = "2022") @Schema(description = "La saison de l'affiliation.", example = "2022")
private int saison; private int saison;
@Schema(description = "Indique si l'affiliation est validée ou non.", example = "true") @Schema(description = "Indique si l'affiliation est validée ou non.", example = "true")
@ -27,7 +27,7 @@ public class SimpleAffiliation {
return new SimpleAffiliationBuilder() return new SimpleAffiliationBuilder()
.id(model.getId()) .id(model.getId())
.club(model.getClub().getId()) .club(String.valueOf(model.getClub().getId()))
.saison(model.getSaison()) .saison(model.getSaison())
.validate(true) .validate(true)
.build(); .build();

View File

@ -36,10 +36,8 @@ public class SimpleClub {
private String contact_intern; private String contact_intern;
@Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris") @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris")
private String address; private String address;
@Schema(description = "RNA du club", example = "W123456789") @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234")
private String RNA; private String state_id;
@Schema(description = "Numéro SIRET du club", example = "12345678901234")
private Long SIRET;
@Schema(description = "Numéro d'affiliation du club", example = "12345") @Schema(description = "Numéro d'affiliation du club", example = "12345")
private Long no_affiliation; private Long no_affiliation;
@Schema(description = "Club international", example = "false") @Schema(description = "Club international", example = "false")
@ -60,8 +58,7 @@ public class SimpleClub {
.training_location(model.getTraining_location()) .training_location(model.getTraining_location())
.training_day_time(model.getTraining_day_time()) .training_day_time(model.getTraining_day_time())
.contact_intern(model.getContact_intern()) .contact_intern(model.getContact_intern())
.RNA(model.getRNA()) .state_id(model.getStateId())
.SIRET(model.getSIRET())
.no_affiliation(model.getNo_affiliation()) .no_affiliation(model.getNo_affiliation())
.international(model.isInternational()) .international(model.isInternational())
.address(model.getAddress()) .address(model.getAddress())

View File

@ -20,8 +20,8 @@ public class SimpleClubList {
String name; String name;
@Schema(description = "Pays du club", example = "FR") @Schema(description = "Pays du club", example = "FR")
String country; String country;
@Schema(description = "Numéro SIRET du club", example = "12345678901234") @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234")
Long siret; String state_id;
@Schema(description = "Numéro d'affiliation du club", example = "12345") @Schema(description = "Numéro d'affiliation du club", example = "12345")
Long no_affiliation; Long no_affiliation;
@ -29,7 +29,7 @@ public class SimpleClubList {
if (model == null) if (model == null)
return null; return null;
return new SimpleClubList(model.getId(), model.getName(), model.getCountry(), model.getSIRET(), return new SimpleClubList(model.getId(), model.getName(), model.getCountry(), model.getStateId(),
model.getNo_affiliation()); model.getNo_affiliation());
} }
} }

View File

@ -25,10 +25,8 @@ public class SimpleReqAffiliation {
Long club_no_aff; Long club_no_aff;
@Schema(description = "Nom du club demander", example = "Association sportive") @Schema(description = "Nom du club demander", example = "Association sportive")
String name; String name;
@Schema(description = "Numéro SIRET de l'association", example = "12345678901234") @Schema(description = "Numéro SIRET ou RNA de l'association", example = "12345678901234")
long siret; String stateId;
@Schema(description = "Numéro RNA de l'association", example = "W123456789")
String RNA;
@Schema(description = "Adresse de l'association", example = "1 rue de l'exemple, 75000 Paris") @Schema(description = "Adresse de l'association", example = "1 rue de l'exemple, 75000 Paris")
String address; String address;
@Schema(description = "Email de contact de l'association", example = "test@test.fr") @Schema(description = "Email de contact de l'association", example = "test@test.fr")
@ -45,8 +43,7 @@ public class SimpleReqAffiliation {
return new SimpleReqAffiliation.SimpleReqAffiliationBuilder() return new SimpleReqAffiliation.SimpleReqAffiliationBuilder()
.id(model.getId()) .id(model.getId())
.name(model.getName()) .name(model.getName())
.siret(model.getSiret()) .stateId(model.getState_id())
.RNA(model.getRNA())
.address(model.getAddress()) .address(model.getAddress())
.saison(model.getSaison()) .saison(model.getSaison())
.contact(model.getContact()) .contact(model.getContact())

View File

@ -16,8 +16,8 @@ public class SimpleReqAffiliationResume {
Long id; Long id;
@Schema(description = "Le nom de l'association.", example = "Association sportive") @Schema(description = "Le nom de l'association.", example = "Association sportive")
String name; String name;
@Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234") @Schema(description = "Le numéro SIRET ou RNA de l'association.", example = "12345678901234")
long siret; String stateId;
@Schema(description = "La saison de l'affiliation.", example = "2025") @Schema(description = "La saison de l'affiliation.", example = "2025")
int saison; int saison;
@ -25,10 +25,10 @@ public class SimpleReqAffiliationResume {
if (model == null) if (model == null)
return null; return null;
return new SimpleReqAffiliationResume.SimpleReqAffiliationResumeBuilder() return new SimpleReqAffiliationResumeBuilder()
.id(model.getId()) .id(model.getId())
.name(model.getName()) .name(model.getName())
.siret(model.getSiret()) .stateId(model.getState_id())
.saison(model.getSaison()) .saison(model.getSaison())
.build(); .build();
} }

View File

@ -33,8 +33,8 @@ public class UniteLegaleRoot {
public String etat_administratif; public String etat_administratif;
public String identifiant_association; public String identifiant_association;
public String nic_siege; public String nic_siege;
public Object nom; public String nom;
public Object nom_usage; public String nom_usage;
public int nombre_periodes; public int nombre_periodes;
public String nomenclature_activite_principale; public String nomenclature_activite_principale;
public Object prenom_1; public Object prenom_1;
@ -67,7 +67,7 @@ public class UniteLegaleRoot {
private Object code_pays_etranger_2; private Object code_pays_etranger_2;
private String code_postal; private String code_postal;
private Object code_postal_2; private Object code_postal_2;
private Object complement_adresse; private String complement_adresse;
private Object complement_adresse2; private Object complement_adresse2;
private String date_creation; private String date_creation;
private String date_debut; private String date_debut;

View File

@ -11,7 +11,7 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.PartType;
@Getter @Getter
@ToString @ToString(exclude = {"status", "logo"})
public class AffiliationRequestForm { public class AffiliationRequestForm {
@Schema(description = "L'identifiant de l'affiliation. (null si nouvelle demande d'affiliation)") @Schema(description = "L'identifiant de l'affiliation. (null si nouvelle demande d'affiliation)")
@FormParam("id") @FormParam("id")
@ -21,13 +21,9 @@ public class AffiliationRequestForm {
@FormParam("name") @FormParam("name")
private String name = null; private String name = null;
@Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234", required = true) @Schema(description = "Le numéro SIRET/RNA de l'association.", example = "12345678901234", required = true)
@FormParam("siret") @FormParam("state_id")
private Long siret = null; private String state_id = null;
@Schema(description = "Le numéro RNA de l'association. (peut être null)", example = "W123456789")
@FormParam("rna")
private String rna = null;
@Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true)
@FormParam("adresse") @FormParam("adresse")
@ -114,8 +110,7 @@ public class AffiliationRequestForm {
public AffiliationRequestModel toModel() { public AffiliationRequestModel toModel() {
AffiliationRequestModel model = new AffiliationRequestModel(); AffiliationRequestModel model = new AffiliationRequestModel();
model.setName(this.getName()); model.setName(this.getName());
model.setSiret(this.getSiret()); model.setState_id(this.getState_id());
model.setRNA(this.getRna());
model.setAddress(this.getAdresse()); model.setAddress(this.getAdresse());
model.setSaison(this.getSaison()); model.setSaison(this.getSaison());
model.setContact(this.getContact()); model.setContact(this.getContact());

View File

@ -4,12 +4,10 @@ import fr.titionfire.ffsaf.utils.RoleAsso;
import jakarta.ws.rs.FormParam; import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import lombok.Getter; import lombok.Getter;
import lombok.ToString;
import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.PartType;
@Getter @Getter
@ToString
public class AffiliationRequestSaveForm { public class AffiliationRequestSaveForm {
@Schema(description = "L'identifiant de l'affiliation.", example = "1", required = true) @Schema(description = "L'identifiant de l'affiliation.", example = "1", required = true)
@FormParam("id") @FormParam("id")
@ -19,13 +17,9 @@ public class AffiliationRequestSaveForm {
@FormParam("name") @FormParam("name")
private String name = null; private String name = null;
@Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234", required = true) @Schema(description = "Le numéro SIRET ou RNA de l'association.", example = "12345678901234", required = true)
@FormParam("siret") @FormParam("state_id")
private Long siret = null; private String state_id = null;
@Schema(description = "Le numéro RNA de l'association. (peut être null)", example = "W123456789")
@FormParam("rna")
private String rna = null;
@Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true)
@FormParam("address") @FormParam("address")
@ -171,4 +165,38 @@ public class AffiliationRequestSaveForm {
} }
} }
} }
@Override
public String toString() {
return "AffiliationRequestSaveForm{" +
"id=" + id +
", name='" + name + '\'' +
", state_id=" + state_id +
", address='" + address + '\'' +
", contact='" + contact + '\'' +
", status_len=" + status.length +
", logo_len=" + logo.length +
", m1_mode=" + m1_mode +
", m1_role=" + m1_role +
", m1_lincence='" + m1_lincence + '\'' +
", m1_lname='" + m1_lname + '\'' +
", m1_fname='" + m1_fname + '\'' +
", m1_email='" + m1_email + '\'' +
", m1_email_mode=" + m1_email_mode +
", m2_mode=" + m2_mode +
", m2_role=" + m2_role +
", m2_lincence='" + m2_lincence + '\'' +
", m2_lname='" + m2_lname + '\'' +
", m2_fname='" + m2_fname + '\'' +
", m2_email='" + m2_email + '\'' +
", m2_email_mode=" + m2_email_mode +
", m3_mode=" + m3_mode +
", m3_role=" + m3_role +
", m3_lincence='" + m3_lincence + '\'' +
", m3_lname='" + m3_lname + '\'' +
", m3_fname='" + m3_fname + '\'' +
", m3_email='" + m3_email + '\'' +
", m3_email_mode=" + m3_email_mode +
'}';
}
} }

View File

@ -43,13 +43,9 @@ public class FullClubForm {
@Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris", required = true) @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris", required = true)
private String address = null; private String address = null;
@FormParam("rna") @FormParam("state_id")
@Schema(description = "RNA du club", example = "W123456789") @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234", required = true)
private String rna = null; private String state_id = null;
@FormParam("siret")
@Schema(description = "Numéro SIRET du club", example = "12345678901234", required = true)
private String siret = null;
@FormParam("international") @FormParam("international")
@Schema(description = "Club international", example = "false", required = true) @Schema(description = "Club international", example = "false", required = true)

View File

@ -43,6 +43,7 @@ notif.affRequest.mail=
siren-api.key=siren-ap siren-api.key=siren-ap
quarkus.rest-client."fr.titionfire.ffsaf.rest.client.SirenService".url=https://data.siren-api.fr/ quarkus.rest-client."fr.titionfire.ffsaf.rest.client.SirenService".url=https://data.siren-api.fr/
quarkus.rest-client."fr.titionfire.ffsaf.rest.client.StateIdService".url=https://www.data-asso.fr/api/
#Login #Login
quarkus.oidc.token-state-manager.split-tokens=true quarkus.oidc.token-state-manager.split-tokens=true

View File

@ -38,18 +38,22 @@ async function getData() {
let icon = null; let icon = null;
if (d.uuid !== null) { if (d.uuid !== null) {
const img = await getMeta(`${api_url}/api/club/${d.uuid}/logo`); try {
let ratio = img.naturalHeight / img.naturalWidth; const img = await getMeta(`${api_url}/api/club/${d.uuid}/logo`);
let ratio = img.naturalHeight / img.naturalWidth;
icon = L.icon({ icon = L.icon({
iconUrl: `${api_url}/api/club/${d.uuid}/logo`, iconUrl: `${api_url}/api/club/${d.uuid}/logo`,
iconSize: [50, 50 * ratio], // size of the icon iconSize: [50, 50 * ratio], // size of the icon
//shadowSize: [50, 64], // size of the shadow //shadowSize: [50, 64], // size of the shadow
iconAnchor: [25, 50 * ratio], // point of the icon which will correspond to marker's location iconAnchor: [25, 50 * ratio], // point of the icon which will correspond to marker's location
//shadowAnchor: [4, 62], // the same for the shadow //shadowAnchor: [4, 62], // the same for the shadow
popupAnchor: [0, -50 * ratio] // point from which the popup should open relative to the iconAnchor popupAnchor: [0, -50 * ratio] // point from which the popup should open relative to the iconAnchor
}); });
}catch (e) {
console.log("Error loading image for club", d.name, e);
}
} }
for (const m of d.training_location) { for (const m of d.training_location) {

View File

@ -1,11 +1,11 @@
import {useEffect, useRef} from 'react' import {useEffect, useRef} from 'react'
import {Nav} from "./components/Nav.jsx"; import {Nav} from "./components/Nav.jsx";
import {createBrowserRouter, Outlet, RouterProvider, useRouteError} from "react-router-dom"; import {createBrowserRouter, Outlet, RouterProvider, useLocation, useRouteError} from "react-router-dom";
import {Home} from "./pages/Homepage.jsx"; import {Home} from "./pages/Homepage.jsx";
import {AdminRoot, getAdminChildren} from "./pages/admin/AdminRoot.jsx"; import {AdminRoot, getAdminChildren} from "./pages/admin/AdminRoot.jsx";
import {AuthCallback} from "./components/auhCallback.jsx"; import {AuthCallback} from "./components/auhCallback.jsx";
import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx"; import {KeycloakContextProvider, useAuth, useAuthDispatch} from "./hooks/useAuth.jsx";
import {check_validity} from "./utils/auth.js"; import {check_validity, login} from "./utils/auth.js";
import {ToastContainer} from "react-toastify"; import {ToastContainer} from "react-toastify";
import './App.css' import './App.css'
@ -15,6 +15,8 @@ import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx"; import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx"; import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx"; import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
import {FallingLines} from "react-loader-spinner";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -119,6 +121,43 @@ function Root() {
theme="light" theme="light"
transition: Flip transition: Flip
/> />
<ReAuthMsg/>
</div>
</>
}
function ReAuthMsg() {
const {is_authenticated} = useAuth()
const location = useLocation()
const notAuthPaths = [
/^\/$/s,
/^\/affiliation(\/)?$/s,
/^\/affiliation\/ok(\/)?$/s,
/^\/complete\/auth.*$/s
]
if (is_authenticated || notAuthPaths.some(r => r.test(location.pathname)))
return <></>
return <>
<div className="overlayBG" style={{position: 'fixed'}}>
<div className="overlayContent" onClick={(e) => {
e.stopPropagation()
}}>
<div className="card">
<div className="card-header">
<h5>Session expirée</h5>
</div>
<div className="card-body">
<p className="card-text">Votre session a expirée, veuillez vous reconnecter pour continuer à
utiliser l'application.</p>
</div>
<div className="card-footer">
<button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>Se reconnecter</button>
<a className="btn btn-secondary" href="/">Accueil</a>
</div>
</div>
</div>
</div> </div>
</> </>
} }

View File

@ -38,7 +38,7 @@ export function HoraireEditor({data}) {
return <div className="row mb-3"> return <div className="row mb-3">
<input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/> <input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Horaires d'entrainements</span> <span className="input-group-text">Horaires d'entraînements</span>
<ul className="list-group form-control"> <ul className="list-group form-control">
{state.map((d, index) => { {state.map((d, index) => {
return <div key={index} className="input-group"> return <div key={index} className="input-group">
@ -92,4 +92,4 @@ export function HoraireEditor({data}) {
</div> </div>
</ul> </ul>
</div> </div>
} }

View File

@ -42,7 +42,7 @@ export function LocationEditor({data, setModal, sendData}) {
return <div className="row mb-3"> return <div className="row mb-3">
<input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/> <input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Lieux d'entrainements</span> <span className="input-group-text">Lieux d'entraînements</span>
<ul className="list-group form-control"> <ul className="list-group form-control">
{state.map((d, index) => { {state.map((d, index) => {
return <div key={index} className="input-group"> return <div key={index} className="input-group">

View File

@ -1,5 +1,5 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {getCategoryFormBirthDate} from "../utils/Tools.js"; import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js";
import {useCountries} from "../hooks/useCountries.jsx"; import {useCountries} from "../hooks/useCountries.jsx";
export function BirthDayField({inti_date, inti_category, required = true}) { export function BirthDayField({inti_date, inti_category, required = true}) {
@ -27,7 +27,7 @@ export function BirthDayField({inti_date, inti_category, required = true}) {
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id="category">Catégorie</span> <span className="input-group-text" id="category">Catégorie</span>
<input type="text" className="form-control" placeholder="" name="category" <input type="text" className="form-control" placeholder="" name="category"
aria-label="category" value={category ? category : ""} aria-describedby="category" aria-label="category" value={category ? getCatName(category) : ""} aria-describedby="category"
disabled/> disabled/>
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1" {canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={updateCat}>Mettre à jours</button>} onClick={updateCat}>Mettre à jours</button>}
@ -88,13 +88,14 @@ export function CountryList({name, text, value, values = undefined, disabled = f
</div> </div>
} }
export function TextField({name, text, value, placeholder, type = "text", disabled = false, required = true}) { export function TextField({name, text, value, placeholder, type = "text", disabled = false, required = true, ttip = null}) {
return <div className="row"> return <div className="row mb-3">
<div className="input-group mb-3"> <div className="input-group">
<span className="input-group-text" id={name}>{text}</span> <span className="input-group-text" id={name}>{text}</span>
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name} <input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required={required}/> name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required={required}/>
</div> </div>
{ttip}
</div> </div>
} }

View File

@ -72,7 +72,7 @@ function ClubMenu() {
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/club/me">Mon club</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/club/me">Mon club</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Member</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/club/member">Membres</NavLink></li>
</ul> </ul>
</li> </li>
} }
@ -88,7 +88,7 @@ function AdminMenu() {
Administration Administration
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Member</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/admin/member">Membres</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/club">Club</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/admin/club">Club</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/stats">Statistiques</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/admin/stats">Statistiques</NavLink></li>
</ul> </ul>

View File

@ -7,8 +7,8 @@ const removeDiacritics = str => {
} }
export function SearchBar({search}) { export function SearchBar({search, defaultValue = ""}) {
const [searchInput, setSearchInput] = useState(""); const [searchInput, setSearchInput] = useState(defaultValue);
const handelChange = (e) => { const handelChange = (e) => {
setSearchInput(e.target.value); setSearchInput(e.target.value);
@ -40,4 +40,4 @@ export function SearchBar({search}) {
</button> </button>
</div> </div>
</div> </div>
} }

View File

@ -3,7 +3,7 @@ import {apiAxios, errFormater, getSaison} from "../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
const notUpperCase = ["de", "la", "le", "les", "des", "du", "d'", "l'", "sur"]; const notUpperCase = ["de", "la", "le", "les", "des", "du", "d'", "l'", "sur", 'lieu', 'dit'];
function formatAdresse(data) { function formatAdresse(data) {
const words = data.split(" "); const words = data.split(" ");
@ -16,28 +16,17 @@ function formatAdresse(data) {
}).join(" "); }).join(" ");
} }
function reconstruireAdresse(infos) { function reconstruireAdresse2(infos) {
console.log(infos);
let adresseReconstruite = ""; let adresseReconstruite = "";
if(infos.numero_voie === null){ if (infos?.complement)
if (infos.complement_adresse) { adresseReconstruite += formatAdresse(infos.complement) + ', ';
adresseReconstruite += formatAdresse(infos.complement_adresse) + ', ';
}
}else{
adresseReconstruite += infos.numero_voie + ' ';
}
adresseReconstruite += formatAdresse(infos.type_voie) + ' '; adresseReconstruite += formatAdresse(infos.voie) + ', ';
adresseReconstruite += formatAdresse(infos.libelle_voie) + ', '; adresseReconstruite += infos.code_postal + ' ' + infos.commune.nom + ', ';
adresseReconstruite += infos.code_postal + ' ' + infos.libelle_commune + ', ';
if (infos.complement_adresse && infos.numero_voie !== null) { if (infos?.pays)
adresseReconstruite += formatAdresse(infos.complement_adresse) + ', '; adresseReconstruite += formatAdresse(infos.pays) + ', ';
}
if (infos.code_cedex && infos.libelle_cedex) {
adresseReconstruite += 'Cedex ' + infos.code_cedex + ' - ' + infos.libelle_cedex;
}
if (adresseReconstruite.endsWith(', ')) { if (adresseReconstruite.endsWith(', ')) {
adresseReconstruite = adresseReconstruite.slice(0, -2); adresseReconstruite = adresseReconstruite.slice(0, -2);
@ -46,6 +35,13 @@ function reconstruireAdresse(infos) {
return adresseReconstruite; return adresseReconstruite;
} }
function getSaisonToAff(currentDate = new Date()) {
if (currentDate.getMonth() >= 7) { //aout et plus
return currentDate.getFullYear()
} else {
return currentDate.getFullYear() - 1
}
}
export function DemandeAff() { export function DemandeAff() {
const {hash} = useLocation(); const {hash} = useLocation();
@ -78,8 +74,7 @@ export function DemandeAff() {
event.preventDefault() event.preventDefault()
const formData = new FormData(event.target) const formData = new FormData(event.target)
formData.append("m1_role", event.target.m1_role?.value) formData.append("m1_role", event.target.m1_role?.value)
formData.append("rna", event.target.rna?.value) formData.append("state_id", event.target.state_id?.value)
formData.append("siret", event.target.siret?.value)
let error = false; let error = false;
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
@ -145,7 +140,7 @@ export function DemandeAff() {
} }
return <div> return <div>
<h1>Demande d'affiliation</h1> <h1>Demande d'affiliation {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}</h1>
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée <p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée
suivante.</p> suivante.</p>
Pour saffilier, une association sportive doit réunir les conditions suivantes : Pour saffilier, une association sportive doit réunir les conditions suivantes :
@ -212,21 +207,23 @@ export function DemandeAff() {
function AssoInfo({initData, needFile}) { function AssoInfo({initData, needFile}) {
const [denomination, setDenomination] = useState("") const [denomination, setDenomination] = useState("")
const [siret, setSiret] = useState(initData.siret ? String(initData.siret) : "") const [stateId, setStateId] = useState(initData.stateId ? String(initData.stateId) : (initData.state_id ? String(initData.state_id) : ""))
const [rna, setRna] = useState(initData.rna ? initData.rna : "")
const [rnaEnable, setRnaEnable] = useState(false)
const [adresse, setAdresse] = useState(initData.address ? initData.address : "") const [adresse, setAdresse] = useState(initData.address ? initData.address : "")
const [saison, setSaison] = useState(initData.saison ? initData.saison : getSaison())
const [contact, setContact] = useState(initData.contact ? initData.contact : "") const [contact, setContact] = useState(initData.contact ? initData.contact : "")
const fetchSiret = () => { const fetchStateId = () => {
if (siret.length < 14) { const regex = /^(?:\d{14}|W\d{9})$/;
toast.error("Le SIRET doit contenir 14 chiffres") let sid = stateId;
if (!regex.test(stateId)) {
toast.error("Le format du SIRET/RNA est invalide");
return; return;
}else{
if (stateId[0] !== 'W')
sid = stateId.substring(0, 9); // Pour les SIRET, on ne garde que les 9 premiers chiffres (SIREN)
} }
toast.promise( toast.promise(
apiAxios.get(`asso/siren/${siret.substring(0, siret.length - 5)}`), apiAxios.get(`asso/state_id/${sid}`),
{ {
pending: "Recherche de l'association en cours", pending: "Recherche de l'association en cours",
success: "Association trouvée avec succès 🎉", success: "Association trouvée avec succès 🎉",
@ -237,34 +234,14 @@ function AssoInfo({initData, needFile}) {
} }
} }
).then(data => { ).then(data => {
const data2 = data.data.unite_legale const data2 = data.data
setDenomination(data2.denomination) setDenomination(data2.identite.nom)
setRnaEnable(data2.identifiant_association === null)
setRna(data2.identifiant_association ? data2.identifiant_association : "")
if (!initData.saison || adresse === "") if (!initData.saison || adresse === "")
setAdresse(reconstruireAdresse(data2.etablissement_siege)) setAdresse(reconstruireAdresse2(data2.coordonnees.adresse_gestion))
}) })
} }
const currentSaison = getSaison();
return <> return <>
<div className="input-group mb-3"> <input name="saison" value={getSaisonToAff()} readOnly hidden/>
<div className="input-group-text">
<input className="form-check-input mt-0" type="radio" value={currentSaison} aria-label={currentSaison + "-" + (currentSaison + 1)}
name={"saison"} checked={saison === currentSaison}
onChange={e => setSaison(Number(e.target.value))}/>
{currentSaison + "-" + (currentSaison + 1)}
</div>
<span className="input-group-text">OU</span>
<div className="input-group-text">
<input className="form-check-input mt-0" type="radio" value={currentSaison + 1}
aria-label={(currentSaison + 1) + "-" + (currentSaison + 2)}
name={"saison"} checked={saison === currentSaison + 1}
onChange={e => setSaison(Number(e.target.value))}/>
{(currentSaison + 1) + "-" + (currentSaison + 2)}
</div>
</div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span> <span className="input-group-text" id="basic-addon1">Nom de l'association*</span>
@ -274,28 +251,21 @@ function AssoInfo({initData, needFile}) {
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">N° SIRET*</span> <span className="input-group-text">N° SIRET ou RNA*</span>
<input type="number" className="form-control" placeholder="siret" name="siret" required value={siret} disabled={!needFile} <input type="text" className="form-control" placeholder="N° SIRET ou RNA*" name="state_id" required value={stateId} disabled={!needFile}
onChange={e => setSiret(e.target.value)}/> onChange={e => setStateId(e.target.value)}/>
<button className="btn btn-outline-secondary" type="button" id="button-addon2" <button className="btn btn-outline-secondary" type="button" id="button-addon2"
onClick={fetchSiret}>Rechercher onClick={fetchStateId} hidden={false}>Rechercher
</button> </button>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3" hidden={false}>
<span className="input-group-text" id="basic-addon1">Dénomination</span> <span className="input-group-text" id="basic-addon1">Dénomination</span>
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter" <input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter"
aria-label="Dénomination" aria-label="Dénomination"
aria-describedby="basic-addon1" disabled value={denomination} readOnly/> aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
</div> </div>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">RNA</span>
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA"
aria-describedby="basic-addon1"
disabled={!rnaEnable} name="rna" value={rna} onChange={e => setRna(e.target.value)}/>
</div>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<span className="input-group-text" id="basic-addon1">Adresse administrative*</span> <span className="input-group-text" id="basic-addon1">Adresse administrative*</span>

View File

@ -5,22 +5,22 @@ export const Home = () => {
return <> return <>
<div className="container"> <div className="container">
<div style={{textAlign: "center", margin: "2em"}}> <div style={{textAlign: "center", margin: "2em"}}>
<h1 className="text-green-800 text-4xl">Bienvenu sur l'intranet de Fédération Française de Soft Armored Fighting</h1> <h1 className="text-green-800 text-4xl">Bienvenue sur lintranet de la Fédération France Soft Armored Fighting</h1>
</div> </div>
<div className="row" style={{marginTop: "3em"}}> <div className="row" style={{marginTop: "3em"}}>
<div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}> <div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}>
<div className="align-content-center" <div className="align-content-center"
style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}> style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}>
<h2><FontAwesomeIcon icon={faUser} size="2xl"/></h2> <h2><FontAwesomeIcon icon={faUser} size="2xl"/></h2>
<h2>Pour les combatants</h2> <h2>Pour les licenciés</h2>
</div> </div>
<p style={{padding: "0.5em 1em 0.5em 1em"}}> <p style={{padding: "0.5em 1em 0.5em 1em"}}>
Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également
télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi qu'en consultée vos résultats sous réserve télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que
que le club organisateur les ait renseignés. <br/> le club organisateur les ait renseignés. <br/>
<br/> <br/>
Lors de votre première inscription, vous réservez un email contenant vos Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé
informations d'identification sur ce site, ce mail sera envoyé une fois votre inscription validée par nos soins. une fois votre licence validée par le secrétariat.
</p> </p>
</div> </div>
<div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}> <div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}>
@ -30,12 +30,12 @@ export const Home = () => {
<h2>Pour les clubs</h2> <h2>Pour les clubs</h2>
</div> </div>
<p style={{padding: "0.5em 1em 0.5em 1em"}}> <p style={{padding: "0.5em 1em 0.5em 1em"}}>
C'est ici que vous pouvez faire l'inscription de vos membres à la fédération, que vous pouvez demander renouveler votre C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre
demande d'affiliation, renseigné vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur le affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur
site ffsaf.fr.<br/> le site ffsaf.fr.<br/>
Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi
que d'un publié les résultats.<br/><br/> que d'enregistrer les résultats.<br/><br/>
Vous n'étes pas encore affilié à la fédération ? Vous pouvez faire une demande d'affiliation en cliquant <a href="/affiliation">içi</a>. Vous n'êtes pas encore affilié à la fédération ? Cliquez <a href="/affiliation">içi</a> pour faire votre première demande.
</p> </p>
</div> </div>
</div> </div>
@ -55,4 +55,4 @@ export const Home = () => {
}}> }}>
</div> </div>
</> </>
}; };

View File

@ -6,38 +6,60 @@ import {useEffect, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import {Checkbox} from "../components/MemberCustomFiels.jsx"; import {Checkbox} from "../components/MemberCustomFiels.jsx";
import * as Tools from "../utils/Tools.js"; import * as Tools from "../utils/Tools.js";
import {apiAxios, errFormater} from "../utils/Tools.js"; import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx"; import {SearchBar} from "../components/SearchBar.jsx";
import * as XLSX from "xlsx-js-style"; import * as XLSX from "xlsx-js-style";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign} from "@fortawesome/free-solid-svg-icons"; import {faEuroSign} from "@fortawesome/free-solid-svg-icons";
let lastRefresh = "";
export function MemberList({source}) { export function MemberList({source}) {
const {hash} = useLocation(); const {hash} = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
let page = Number(hash.substring(1));
page = (page > 0) ? page : 1;
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]); const [licenceData, setLicenceData] = useState([]);
const [showLicenceState, setShowLicenceState] = useState(false); const [showLicenceState, setShowLicenceState] = useState(false);
const [clubFilter, setClubFilter] = useState("");
const [stateFilter, setStateFilter] = useState(4) const setFilter = (filter) => {
const [lastSearch, setLastSearch] = useState(""); navigate("#" + encodeURI(JSON.stringify(filter)))
const [paymentFilter, setPaymentFilter] = useState(2); }
const filter = {
page: 1,
search: "",
club: "",
licenceRequest: 4,
payment: 2,
order: "",
categorie: "",
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
}
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1) const {
data,
error,
refresh
} = useFetch(`/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`, setLoading, 1)
useEffect(() => { useEffect(() => {
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`); const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`;
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]); if (tmp === lastRefresh)
return;
lastRefresh = tmp
refresh(lastRefresh);
}, [hash]);
useEffect(() => { useEffect(() => {
if (!data) if (!data)
return; return;
if (data.page_count < filter.page) {
setFilter({...filter, page: 1});
}
const data2 = []; const data2 = [];
for (const e of data.result) { for (const e of data.result) {
data2.push({ data2.push({
@ -74,19 +96,19 @@ export function MemberList({source}) {
}, [showLicenceState]); }, [showLicenceState]);
const search = (search) => { const search = (search) => {
if (search === lastSearch) if (search === filter.search)
return; return;
setLastSearch(search); setFilter({...filter, search: search});
} }
return <> return <>
<div> <div>
<div className="row"> <div className="row">
<div className="col-lg-9"> <div className="col-lg-9">
<SearchBar search={search}/> <SearchBar search={search} defaultValue={filter.search}/>
{data {data
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState} ? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState}
page={page} source={source}/> page={filter.page} setPage={e => setFilter({...filter, page: e})} source={source}/>
: error : error
? <AxiosError error={error}/> ? <AxiosError error={error}/>
: <Def/> : <Def/>
@ -102,13 +124,28 @@ export function MemberList({source}) {
<button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des <button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des
licences</button>} licences</button>}
</div> </div>
<div className="card mb-4">
<div className="card-header">Trie</div>
<div className="card-body">
<OrderBar onOrderChange={e => setFilter({...filter, order: e.join(",")})} defaultValues={filter.order} source={source}/>
</div>
</div>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Filtre</div> <div className="card-header">Filtre</div>
<div className="card-body"> <div className="card-body">
<FiltreBar showLicenceState={showLicenceState} setShowLicenceState={setShowLicenceState} data={data} <FiltreBar showLicenceState={showLicenceState}
clubFilter={clubFilter} setClubFilter={setClubFilter} source={source} setShowLicenceState={setShowLicenceState}
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter} clubFilter={filter.club}
setPaymentFilter={setPaymentFilter}/> setClubFilter={e => setFilter({...filter, club: e})}
source={source}
stateFilter={filter.licenceRequest}
setStateFilter={e => setFilter({...filter, licenceRequest: e})}
paymentFilter={filter.payment}
setPaymentFilter={e => setFilter({...filter, payment: e})}
catFilter={filter.categorie}
setCatFilter={e => setFilter({...filter, categorie: e})}/>
</div> </div>
</div> </div>
@ -169,6 +206,10 @@ function FileOutput() {
certifDate: e.certif ? new Date(e.certif.split("¤")[1]) : '', certifDate: e.certif ? new Date(e.certif.split("¤")[1]) : '',
} }
if (isNaN(tmp.certifDate) || tmp.certifDate === 'NaN') {
tmp.certifDate = ''
}
//tmp.birthdate.setMilliseconds(0); //tmp.birthdate.setMilliseconds(0);
//tmp.birthdate.setSeconds(0); //tmp.birthdate.setSeconds(0);
//tmp.birthdate.setMinutes(0); //tmp.birthdate.setMinutes(0);
@ -332,11 +373,11 @@ function FileInput() {
); );
} }
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, source}) { function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) {
const pages = [] const pages = []
for (let i = 1; i <= data.page_count; i++) { for (let i = 1; i <= data.page_count; i++) {
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}> pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
<span className="page-link" onClick={() => navigate("#" + i)}>{i}</span> <span className="page-link" onClick={() => setPage(i)}>{i}</span>
</li>); </li>);
} }
@ -353,10 +394,10 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul className="pagination justify-content-center"> <ul className="pagination justify-content-center">
<li className={"page-item" + ((page <= 1) ? " disabled" : "")}> <li className={"page-item" + ((page <= 1) ? " disabled" : "")}>
<span className="page-link" onClick={() => navigate("#" + (page - 1))}>&laquo;</span></li> <span className="page-link" onClick={() => setPage(page - 1)}>&laquo;</span></li>
{pages} {pages}
<li className={"page-item" + ((page >= data.page_count) ? " disabled" : "")}> <li className={"page-item" + ((page >= data.page_count) ? " disabled" : "")}>
<span className="page-link" onClick={() => navigate("#" + (page + 1))}>&raquo;</span></li> <span className="page-link" onClick={() => setPage(page + 1)}>&raquo;</span></li>
</ul> </ul>
</nav> </nav>
</div> </div>
@ -365,45 +406,146 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
function MakeRow({member, showLicenceState, navigate, source}) { function MakeRow({member, showLicenceState, navigate, source}) {
const rowContent = <> const rowContent = <>
<div className="row"> <div className="row" style={{padding: "0.6em 0"}}>
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "} <span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "}
{(showLicenceState && member.licence != null && member.licence.pay)? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span> {(showLicenceState && member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span>
<div className="ms-2 col-auto"> <div className="ms-2 col-auto">
<div className="fw-bold">{member.fname} {member.lname}</div> <div className="fw-bold">{member.fname} {member.lname}</div>
</div> </div>
</div> </div>
{source === "club" ? <div style={{verticalAlign: "center", margin: "auto 0"}}>
<small>{member.categorie}</small> {source === "club" ?
: <small>{member.club?.name || "Sans club"}</small>} <small>{getCatName(member.categorie)}</small>
: <div style={{
textAlign: "right",
fontSize: "small"
}}>{member.club?.name || "Sans club"}<br/><small>{getCatName(member.categorie)}</small></div>}
</div>
</> </>
if (showLicenceState && member.licence != null) { if (showLicenceState && member.licence != null) {
return <div return <a className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-"
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-" + (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))}
+ (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))} style={{padding: "0 1em"}}
onClick={() => navigate("" + member.id)}>{rowContent}</div> onClick={e => {
} else { e.preventDefault();
return <div className="list-group-item d-flex justify-content-between align-items-start list-group-item-action" navigate("" + member.id)
onClick={() => navigate("" + member.id)}> }}
href={"member/" + member.id}>
{rowContent} {rowContent}
</div> </a>
} else {
return <a className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
style={{padding: "0 1em"}}
onClick={e => {
e.preventDefault();
navigate("" + member.id)
}}
href={"member/" + member.id}>
{rowContent}
</a>
} }
} }
let allClub = [] function OrderBar({onOrderChange, defaultValues = "", source}) {
const [orderCriteria, setOrderCriteria] = useState([...defaultValues.split(",").filter(c => c !== ''), '']);
function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) { const handleChange = (index, value) => {
useEffect(() => { const newCriteria = [...orderCriteria];
if (!data) newCriteria[index] = value;
return;
allClub.push(...data.result.map((e) => e.club?.name)) // Si le dernier critère est rempli, on en ajoute un nouveau
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() if (index === orderCriteria.length - 1 && value !== '') {
}, [data]); newCriteria.push('');
}
// Si un critère (sauf le premier) est réinitialisé, on le supprime
else if (value === '' && (index !== 0 || orderCriteria.length > 1)) {
newCriteria.splice(index, 1);
}
setOrderCriteria(newCriteria);
onOrderChange(newCriteria.filter(c => c !== ''));
};
// Liste de toutes les options possibles
const allOptions = [
{value: 'lname n', label: 'Nom ↓', base: 'lname'},
{value: 'lname i', label: 'Nom ↑', base: 'lname'},
{value: 'fname n', label: 'Prénom ↓', base: 'fname'},
{value: 'fname i', label: 'Prénom ↑', base: 'fname'},
{value: 'categorie n', label: 'Catégorie ↓', base: 'categorie'},
{value: 'categorie i', label: 'Catégorie ↑', base: 'categorie'},
{value: 'licence n', label: 'Licence ↓', base: 'licence'},
{value: 'licence i', label: 'Licence ↑', base: 'licence'},
];
if (source === "admin") {
allOptions.push(
{value: 'club.name n', label: 'Club ↓', base: 'club.name'},
{value: 'club.name i', label: 'Club ↑', base: 'club.name'},
);
}
return (
<div className="mb-3">
{orderCriteria.map((criteria, index) => {
// Récupère les bases des critères déjà sélectionnés (sauf le courant)
const usedBases = orderCriteria
.filter((c, i) => c !== '' && i !== index)
.map(c => allOptions.find(o => o.value === c)?.base);
// Filtre les options disponibles
const availableOptions = allOptions.filter(option =>
!usedBases.includes(option.base) || option.value === criteria
);
return (
<select
key={index}
className="form-select mb-2"
value={criteria}
onChange={(e) => handleChange(index, e.target.value)}
>
<option value="">----</option>
{availableOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
})}
</div>
);
}
function FiltreBar({
showLicenceState,
setShowLicenceState,
clubFilter,
setClubFilter,
source,
stateFilter,
setStateFilter,
paymentFilter,
setPaymentFilter,
catFilter,
setCatFilter,
}) {
return <div> return <div>
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/> <Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
</div> </div>
<div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option>
{Tools.CatList.map(cat => (
<option key={cat} value={cat}>{getCatName(cat)}</option>
))}
</select>
</div>
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>} {source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}> <select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>

View File

@ -68,7 +68,7 @@ function MakeRow({request, navigate}) {
<div className="ms-2 col-auto"> <div className="ms-2 col-auto">
<div className="fw-bold">{request.name}</div> <div className="fw-bold">{request.name}</div>
</div> </div>
<small style={{textAlign: 'right'}}>{request.saison}-{request.saison + 1}<br/>{request.siret}</small> <small style={{textAlign: 'right'}}>{request.saison}-{request.saison + 1}<br/>{request.state_id}</small>
</div> </div>
} }
@ -104,4 +104,4 @@ function Def() {
<li className="list-group-item"><ThreeDots/></li> <li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li> <li className="list-group-item"><ThreeDots/></li>
</div> </div>
} }

View File

@ -8,7 +8,6 @@ import {RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -67,8 +66,7 @@ function Content({data, refresh}) {
formData.append('id', data.id); formData.append('id', data.id);
formData.append('name', event.target.name.value); formData.append('name', event.target.name.value);
formData.append('siret', event.target.siret.value); formData.append('state_id', event.target.state_id.value);
formData.append('rna', event.target.rna.value);
formData.append('address', event.target.address.value); formData.append('address', event.target.address.value);
formData.append('contact', event.target.contact.value); formData.append('contact', event.target.contact.value);
@ -166,7 +164,7 @@ function Content({data, refresh}) {
<input name="id" value={data.id} readOnly hidden/> <input name="id" value={data.id} readOnly hidden/>
<div className="card-header">Demande d'affiliation</div> <div className="card-header">Demande d'affiliation</div>
<div className="card-body text-center"> <div className="card-body text-center">
{data.club && <h5>Ce club a déjà ete affilier (affiliation n°{data.club_no_aff})</h5>} {data.club && <h5>Ce club a déjà été affilié (affiliation n°{data.club_no_aff})</h5>}
<h4 id="saison">Saison {data.saison}-{data.saison + 1}</h4> <h4 id="saison">Saison {data.saison}-{data.saison + 1}</h4>
<div className="row mb-3"> <div className="row mb-3">
@ -178,8 +176,7 @@ function Content({data, refresh}) {
{data.club && <div className="form-text" id="name">Ancien nom: {data.club_name}</div>} {data.club && <div className="form-text" id="name">Ancien nom: {data.club_name}</div>}
</div> </div>
<TextField type="number" name="siret" text="SIRET" value={data.siret} disabled={true}/> <TextField name="state_id" text="SIRET ou RNA" value={data.stateId} disabled={true}/>
<TextField name="rna" text="RNA" value={data.rna} required={false}/>
<TextField name="address" text="Adresse" value={data.address}/> <TextField name="address" text="Adresse" value={data.address}/>
<TextField name="contact" text="Contact administratif" value={data.contact}/> <TextField name="contact" text="Contact administratif" value={data.contact}/>

View File

@ -40,7 +40,7 @@ export function ClubList() {
country: e.country, country: e.country,
siret: e.siret, siret: e.siret,
no_affiliation: e.no_affiliation, no_affiliation: e.no_affiliation,
affiliation: showAffiliationState ? affiliationData.find(aff => (aff.id >= 0) ? aff.club === e.id : aff.club === e.siret) : null affiliation: showAffiliationState ? affiliationData.find(aff => (aff.id >= 0) ? Number(aff.club) === e.id : aff.club === e.state_id) : null
}) })
} }
setClubData(data2); setClubData(data2);
@ -197,4 +197,4 @@ function Def() {
<li className="list-group-item"><ThreeDots/></li> <li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li> <li className="list-group-item"><ThreeDots/></li>
</div> </div>
} }

View File

@ -130,8 +130,7 @@ function InformationForm({data}) {
</div> </div>
</div> </div>
{!switchOn && <> {!switchOn && <>
<TextField name="siret" text="SIRET" value={data.siret} required={false} type="number"/> <TextField name="state_id" text="SIRET ou RNA" value={data.state_id} required={false}/>
<TextField name="rna" text="RNA" value={data.rna} required={false}/>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false} <TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}
placeholder="example@test.com"/> placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" value={data.address} required={false} <TextField name="address" text="Adresse administrative" value={data.address} required={false}
@ -172,6 +171,7 @@ function InformationForm({data}) {
export function BureauCard({clubData}) { export function BureauCard({clubData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1) const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1)
const navigate = useNavigate();
return <> return <>
<div className="card mb-4"> <div className="card mb-4">
@ -179,7 +179,8 @@ export function BureauCard({clubData}) {
<div className="card-body"> <div className="card-body">
<ul className="list-group"> <ul className="list-group">
{data && data.map((d, index) => { {data && data.map((d, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start"> return <div key={index} className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
onClick={__ => navigate(`/admin/member/${d.id}`)}>
<div className="me-auto"><small>{d.role}</small><br/>{d.lname} {d.fname}</div> <div className="me-auto"><small>{d.role}</small><br/>{d.lname} {d.fname}</div>
</div> </div>
})} })}
@ -188,4 +189,4 @@ export function BureauCard({clubData}) {
</div> </div>
{error && <AxiosError error={error}/>} {error && <AxiosError error={error}/>}
</> </>
} }

View File

@ -84,8 +84,7 @@ function InformationForm() {
</div> </div>
</div> </div>
{!switchOn && <> {!switchOn && <>
<TextField name="siret" text="SIRET" required={false} type="number"/> <TextField name="state_id" text="SIRET ou RNA" required={false}/>
<TextField name="rna" text="RNA" required={false}/>
<TextField name="contact_intern" text="Contact interne" required={false} placeholder="example@test.com"/> <TextField name="contact_intern" text="Contact interne" required={false} placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" required={false} placeholder="Adresse administrative"/> <TextField name="address" text="Adresse administrative" required={false} placeholder="Adresse administrative"/>

View File

@ -34,13 +34,13 @@ export function MemberPage() {
} }
} }
).then(_ => { ).then(_ => {
navigate("/admin/member") navigate(-1)
}) })
} }
return <> return <>
<h2>Page membre</h2> <h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}> <button type="button" className="btn btn-link" onClick={() => navigate(-1)}>
&laquo; retour &laquo; retour
</button> </button>
{data {data

View File

@ -23,7 +23,7 @@ export function ClubRoot() {
return <> return <>
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}> <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}>
<h1>Espace club</h1><h3 style={{marginLeft: '0.75em'}}>{club}</h3></div> <h3 style={{marginLeft: '0.75em'}}>Club: {club}</h3></div>
<LoadingProvider> <LoadingProvider>
<Outlet/> <Outlet/>
</LoadingProvider> </LoadingProvider>

View File

@ -1,12 +1,11 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {useEffect, useReducer, useState} from "react"; import {useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEye, faFilePdf, faPen} from "@fortawesome/free-solid-svg-icons"; import {faEye, faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, getSaison} from "../../../utils/Tools.js"; import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -42,8 +41,8 @@ export function AffiliationCard({clubData}) {
<a href={`${vite_url}/api/club/me/affiliation`} target='#'> <a href={`${vite_url}/api/club/me/affiliation`} target='#'>
<button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}} <button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}}
onClick={e => null}> onClick={_ => null}>
Téléchargée l'attestation d'affiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> Télécharger lattestation daffiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
</div> </div>
@ -140,10 +139,10 @@ function ModalContent2({clubData, data}) {
} }
} }
if (list.length !== 3) { while (list.length < 3) {
toast.error("Il faut sélectionner 3 membres pour renouveler l'affiliation") list.push(-1)
return
} }
apiAxios.get(`/club/renew/${clubData.id}?m1=${list[0]}&m2=${list[1]}&m3=${list[2]}`).then(data => { apiAxios.get(`/club/renew/${clubData.id}?m1=${list[0]}&m2=${list[1]}&m3=${list[2]}`).then(data => {
navigate('/affiliation#d' + encodeURI(JSON.stringify(data.data))) navigate('/affiliation#d' + encodeURI(JSON.stringify(data.data)))
}) })

View File

@ -1,9 +1,7 @@
import {useNavigate, useParams} from "react-router-dom";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater} from "../../../utils/Tools.js";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {AffiliationCard, BureauCard} from "./AffiliationCard.jsx"; import {AffiliationCard, BureauCard} from "./AffiliationCard.jsx";
import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
@ -22,7 +20,7 @@ export function MyClubPage() {
const {data, error} = useFetch(`/club/me`, setLoading, 1) const {data, error} = useFetch(`/club/me`, setLoading, 1)
return <> return <>
<h2>Mon club</h2> <h3>Données administratives</h3>
{data {data
? <div> ? <div>
<div className="row"> <div className="row">
@ -77,8 +75,7 @@ function InformationForm({data}) {
<CountryList name="country" text="Pays" value={data.country} disabled={true}/> <CountryList name="country" text="Pays" value={data.country} disabled={true}/>
{!data.international && <> {!data.international && <>
<TextField name="siret" text="SIRET" value={data.siret} type="number" disabled={true}/> <TextField name="state_id" text="SIRET ou RNA" value={data.state_id} disabled={true}/>
<TextField name="rna" text="RNA" value={data.rna} required={false} disabled={true}/>
</>} </>}
<div className="row mb-3"> <div className="row mb-3">
@ -91,7 +88,7 @@ function InformationForm({data}) {
<div className="col-md-6"> <div className="col-md-6">
<a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'> <a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'>
<button className="btn btn-outline-secondary" type="button" id="button-addon1" <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={e => null}> onClick={_ => null}>
<FontAwesomeIcon icon={faFilePdf} size="5x"></FontAwesomeIcon><br/> <FontAwesomeIcon icon={faFilePdf} size="5x"></FontAwesomeIcon><br/>
Voir les statues Voir les statues
</button> </button>

View File

@ -49,7 +49,9 @@ export function InformationForm({data}) {
<TextField name="lname" text="Nom" value={data.lname}/> <TextField name="lname" text="Nom" value={data.lname}/>
<TextField name="fname" text="Prénom" value={data.fname}/> <TextField name="fname" text="Prénom" value={data.fname}/>
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com" <TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
type="email"/> type="email" ttip={<small className="form-text">L'email sert à la création de compte pour se connecter au site et doit être unique. <br/>
Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+<caractères alphanumériques>@exemple.com'}.<br/>
Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com</small>}/>
<OptionField name="genre" text="Genre" value={data.genre} <OptionField name="genre" text="Genre" value={data.genre}
values={{NA: 'N/A', H: 'H', F: 'F'}}/> values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<CountryList name="country" text="Pays" value={data.country}/> <CountryList name="country" text="Pays" value={data.country}/>

View File

@ -33,13 +33,13 @@ export function MemberPage() {
} }
} }
).then(_ => { ).then(_ => {
navigate("/club/member") navigate(-1)
}) })
} }
return <> return <>
<h2>Page membre</h2> <h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}> <button type="button" className="btn btn-link" onClick={() => navigate(-1)}>
&laquo; retour &laquo; retour
</button> </button>
{data {data

View File

@ -23,6 +23,20 @@ export const errFormater = (data, msg) => {
return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕` return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕`
} }
export const CatList = [
"SUPER_MINI",
"MINI_POUSSIN",
"POUSSIN",
"BENJAMIN",
"MINIME",
"CADET",
"JUNIOR",
"SENIOR1",
"SENIOR2",
"VETERAN1",
"VETERAN2"
];
export function getCategoryFormBirthDate(birth_date, currentDate = new Date()) { export function getCategoryFormBirthDate(birth_date, currentDate = new Date()) {
const currentSaison = getSaison(currentDate) const currentSaison = getSaison(currentDate)
const birthYear = birth_date.getFullYear() const birthYear = birth_date.getFullYear()
@ -60,3 +74,32 @@ export function getSaison(currentDate = new Date()) {
return currentDate.getFullYear() - 1 return currentDate.getFullYear() - 1
} }
} }
export function getCatName(cat) {
switch (cat) {
case "SUPER_MINI":
return "Super Mini";
case "MINI_POUSSIN":
return "Mini Poussin";
case "POUSSIN":
return "Poussin";
case "BENJAMIN":
return "Benjamin";
case "MINIME":
return "Minime";
case "CADET":
return "Cadet";
case "JUNIOR":
return "Junior";
case "SENIOR1":
return "Senior 1";
case "SENIOR2":
return "Senior 2";
case "VETERAN1":
return "Vétéran 1";
case "VETERAN2":
return "Vétéran 2";
default:
return cat;
}
}

View File

@ -9,8 +9,11 @@ export function check_validity(online_callback = () => {
axios.get(`${vite_url}/api/auth/userinfo`).then(data => { axios.get(`${vite_url}/api/auth/userinfo`).then(data => {
online_callback({state: true, userinfo: data.data}); online_callback({state: true, userinfo: data.data});
}) })
}else{
online_callback({state: false});
} }
}).catch(() => { }).catch(() => {
console.log("=> Not authenticated");
online_callback({state: false}); online_callback({state: false});
}) })
} }
@ -32,4 +35,4 @@ export function login_redirect() {
export function logout() { export function logout() {
window.location.href = `${vite_url}/api/logout`; window.location.href = `${vite_url}/api/logout`;
} }

View File

@ -24,4 +24,4 @@ export default ({mode}) => {
}, },
}, },
}); });
}; };