Compare commits

...

53 Commits

Author SHA1 Message Date
a8565534e6 update README.md 2025-12-05 15:28:03 +01:00
43fcbb5ef6 Merge pull request 'feat: re-add siret/rna api' (#68) from dev into master
Reviewed-on: #68
2025-11-19 14:31:46 +00:00
fefc5d651b feat: re-add siret/rna api
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m4s
2025-11-19 15:26:36 +01:00
163d9b23e9 Merge pull request 'fix: error on loading logo on maps' (#67) from dev into master
Reviewed-on: #67
2025-11-18 21:02:58 +00:00
53509820c6 fix: error on loading logo on maps
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m58s
2025-11-18 22:02:13 +01:00
a8ad3a7558 Merge pull request 'fix: remove siret/rna api' (#66) from dev into master
Reviewed-on: #66
2025-11-18 20:46:34 +00:00
f46e268d39 fix: remove siret/rna api
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 1m33s
2025-11-18 21:46:05 +01:00
abad3194a4 Merge pull request 'dev' (#65) from dev into master
Reviewed-on: #65
2025-11-18 19:32:14 +00:00
6e7eec0587 feat: membre list filter history
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-11-18 15:45:18 +01:00
8c83668364 feat: add categorie filter membre search 2025-11-18 14:02:02 +01:00
771c06ccd8 feat: make user-friendly cat name 2025-11-18 13:27:49 +01:00
b479b992cf feat: add ordering for member page 2025-11-17 21:48:24 +01:00
f018e52afa Merge pull request 'dev' (#64) from dev into master
Reviewed-on: #64
2025-11-14 15:55:53 +00:00
be2f01c070 feat: upgrade club groupe name in kc when club is rename
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m13s
2025-11-14 16:38:44 +01:00
9ab50238b9 feat: upgrade aff find to user all id (Rna, siret, siren) 2025-11-14 16:17:40 +01:00
cf5d93630f fix: certifDate NaN all membre export 2025-11-14 14:46:58 +01:00
e5e17d3862 Merge pull request 'fix: null licence on import' (#63) from dev into master
Reviewed-on: #63
2025-11-14 13:32:09 +00:00
7410569ced fix: null licence on import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m3s
2025-11-14 14:31:37 +01:00
b107f443aa Merge pull request 'fix: add more log to import' (#62) from dev into master
Reviewed-on: #62
2025-11-14 13:15:27 +00:00
8e2d68ebd5 fix: add more log to import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m10s
2025-11-14 14:14:22 +01:00
f050127fd7 Merge pull request 'dev' (#61) from dev into master
Reviewed-on: #61
2025-11-14 12:45:53 +00:00
d02fd63834 fix: empty mail on import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m30s
2025-11-14 13:43:51 +01:00
b143cc759f feat: remove kc new account mail 2025-11-12 16:38:35 +01:00
cc5534ef00 feat: add cache to getAssoInfo 2025-11-12 16:15:40 +01:00
26f56006f6 feat: merge rna and siret fields 2025-11-12 14:56:28 +01:00
f8dacee3e7 Merge pull request 'fix: club delete on RegisterModel' (#60) from dev into master
Reviewed-on: #60
2025-11-08 18:09:04 +00:00
ed1f30f2b6 fix: club delete on RegisterModel
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m17s
2025-11-08 19:08:41 +01:00
8517e9824c Merge pull request 'feat: allow empty mail' (#59) from dev into master
Reviewed-on: #59
2025-11-08 17:45:02 +00:00
6cec8ff31d feat: allow empty mail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-11-08 18:44:35 +01:00
e7deba52e9 Merge pull request 'dev' (#58) from dev into master
Reviewed-on: #58
2025-11-07 15:31:18 +00:00
d95c173fa8 fix: aff renew select length
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m51s
2025-11-07 16:19:49 +01:00
8a0e4423f2 feat: remove saison selection on aff req 2025-11-07 16:13:37 +01:00
b956236934 fix: affiliation ok login msg 2025-11-07 15:57:36 +01:00
7767c98304 feat: add email tooltip 2025-11-07 15:55:10 +01:00
7e380ccb69 feat: add re-login message 2025-11-07 15:31:25 +01:00
94d1148eb1 fix: null email on import 2025-11-07 15:30:56 +01:00
8a14f58ce5 Merge pull request 'fix: log detail' (#57) from dev into master
Reviewed-on: #57
2025-11-05 21:06:54 +00:00
79dbbdaaec fix: log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-11-05 22:06:36 +01:00
bf5704db54 Merge pull request 'fix: log detail' (#56) from dev into master
Reviewed-on: #56
2025-11-05 20:43:26 +00:00
dec98f9508 fix: log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m22s
2025-11-05 21:43:05 +01:00
d3a62e980d Merge pull request 'feat: add aff req log detail' (#55) from dev into master
Reviewed-on: #55
2025-11-05 20:19:10 +00:00
b89ed62795 feat: add aff req log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m33s
fix: membre import null email filter
2025-11-05 21:18:29 +01:00
5ffc9fb495 Merge pull request 'fix: typo' (#54) from dev into master
Reviewed-on: #54
2025-09-05 18:31:45 +00:00
5e48bc4623 fix: typo
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m51s
2025-09-05 20:31:10 +02:00
81b953fb05 Merge pull request 'dev' (#53) from dev into master
Reviewed-on: #53
2025-09-03 19:47:13 +00:00
c6659f8d85 feat: keep log
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m19s
2025-09-03 21:46:26 +02:00
e86fe42b3d fix: club order 2025-09-03 21:35:36 +02:00
ef528aa524 fix: log message length 2025-09-03 19:53:19 +02:00
e6cc4cbc96 Merge pull request 'dev' (#52) from dev into master
Reviewed-on: #52
2025-08-21 09:43:54 +00:00
c58dedf80a Merge pull request 'competition rework' (#51) from dev into master
Reviewed-on: #51
2025-08-18 09:33:51 +00:00
1908de681e Merge pull request 'dev' (#50) from dev into master
Reviewed-on: #50
2025-08-15 19:49:13 +00:00
76381c75bd Merge pull request 'fix: Membre edition' (#49) from dev into master
Reviewed-on: #49
2025-08-15 18:34:43 +00:00
7d281196c1 Merge pull request 'dev' (#48) from dev into master
Reviewed-on: #48
2025-08-15 16:26:23 +00:00
56 changed files with 837 additions and 387 deletions

View File

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

View File

@ -1,8 +1,22 @@
# ffsaf-site
# FFSAF - Intranet
This project uses Quarkus, the Supersonic Subatomic Java Framework.
## Introduction et context
If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
Lintranet de la Fédération française de Soft Armored Fighting a pour but centralise la prise de licences des adhérents
et laffiliation des clubs. Il permet aussi de recueillir les résultats des compétitions organisées par les clubs et,
plus récemment, den faciliter lorganisation grâce à un outil intégré.
Le système de prise de licence se divise en trois parties :
* Un formulaire public pour la première demande daffiliation ;
* Un espace club, accessible après validation, pour la saisie des informations relatives aux demandes de licence des
adhérents ;
* Un espace fédération pour accepter les demandes de licences et daffiliation.
Un espace membre permet enfin à chaque adhérent de télécharger son attestation de licence.
Pour les compétitions, le système permet la création de catégories, de poules et de matchs, avec génération automatique
des matchs au sein dune poule. Il supporte plusieurs lices, assure la synchronisation en temps réel des modifications
entre toutes les instances de lapplication web et publie automatiquement les résultats sur le site de lorganisateur.
## Running the application in dev mode
@ -51,35 +65,4 @@ Or, if you don't have GraalVM installed, you can run the native executable build
You can then execute your native executable with: `./target/ffsaf-site-1.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.
## Related Guides
- Reactive MySQL client ([guide](https://quarkus.io/guides/reactive-sql-clients)): Connect to the MySQL database using the reactive pattern
- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A Jakarta REST implementation utilizing build time processing and Vert.x.
This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
- Hibernate ORM with Panache ([guide](https://quarkus.io/guides/hibernate-orm-panache)): Simplify your persistence code for Hibernate ORM via the
active record or the repository pattern
- Reactive PostgreSQL client ([guide](https://quarkus.io/guides/reactive-sql-clients)): Connect to the PostgreSQL database using the reactive pattern
## Provided Code
### Hibernate ORM
Create your first JPA entity
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
### RESTEasy Reactive
Easily start your Reactive RESTful Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
### RESTEasy Reactive Qute
Create your web page using Quarkus RESTEasy Reactive & Qute
[Related guide section...](https://quarkus.io/guides/qute#type-safe-templates)
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.

View File

@ -21,8 +21,7 @@ public class AffiliationRequestModel {
Long id;
String name;
long siret;
String RNA;
String state_id;
String address;
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")
String address;
@Schema(description = "RNA du club", example = "W123456789")
String RNA;
@Schema(description = "Numéro SIRET du club", example = "12345678901234")
Long SIRET;
@Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234")
String StateId;
@Schema(description = "Numéro d'affiliation du club", example = "12345")
Long no_affiliation;

View File

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

View File

@ -82,4 +82,22 @@ public class MembreModel implements LoggableModel {
public LogModel.ObjectType getObjectType() {
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.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
@Getter
@Setter
@ -38,6 +40,7 @@ public class RegisterModel {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "club")
@OnDelete(action = OnDeleteAction.SET_NULL)
ClubModel club = null;
@Column(nullable = false, columnDefinition = "boolean default false")

View File

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

View File

@ -2,6 +2,8 @@ package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
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.SimpleReqAffiliation;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
@ -21,15 +23,19 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
@WithSession
@ApplicationScoped
public class AffiliationService {
private static final Logger LOGGER = Logger.getLogger(AffiliationService.class);
@Inject
CombRepository combRepository;
@ -58,6 +64,12 @@ public class AffiliationService {
@Inject
LoggerService ls;
@RestClient
StateIdService stateIdService;
@RestClient
SirenService sirenService;
@ConfigProperty(name = "upload_dir")
String media;
@ -71,6 +83,8 @@ public class AffiliationService {
public Uni<AffiliationRequestModel> pre_save(AffiliationRequestForm form, boolean unique) {
AffiliationRequestModel affModel = form.toModel();
int currentSaison = Utils.getSaison();
List<String> out = new ArrayList<>();
out.add(affModel.getState_id());
return Uni.createFrom().item(affModel)
.invoke(Unchecked.consumer(model -> {
@ -78,14 +92,26 @@ public class AffiliationService {
throw new DBadRequestException("Saison non valid");
}
}))
.chain(() -> repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(),
affModel.getSaison()))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0 && unique) {
throw new DBadRequestException("Demande d'affiliation déjà existante");
}
}))
.chain(() -> clubRepository.find("SIRET = ?1", affModel.getSiret()).firstResult().chain(club ->
.chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna(
affModel.getState_id()) : sirenService.get_unite(affModel.getState_id())
.chain(stateIdService::getAssoDataFromUnit)).onItem().transform(o -> {
if (o.getRna() != null && !o.getRna().isBlank())
out.add(o.getRna());
if (o.getSiren() != null && !o.getSiren().isBlank())
out.add(o.getSiren());
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())))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0) {
@ -122,7 +148,6 @@ public class AffiliationService {
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.chain(origine -> {
origine.setName(model.getName());
origine.setRNA(model.getRNA());
origine.setAddress(model.getAddress());
origine.setContact(model.getContact());
origine.setM1_lname(model.getM1_lname());
@ -146,6 +171,9 @@ public class AffiliationService {
}
public Uni<String> save(AffiliationRequestForm form) {
LOGGER.debug("Affiliation Request Created");
LOGGER.debug(form.toString());
// noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher
return pre_save(form, true)
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
@ -169,12 +197,14 @@ public class AffiliationService {
}
public Uni<?> saveAdmin(AffiliationRequestSaveForm form) {
LOGGER.debug("Affiliation Request Saved");
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.map(model -> {
model.setName(form.getName());
model.setSiret(form.getSiret());
model.setRNA(form.getRna());
model.setState_id(form.getState_id());
model.setAddress(form.getAddress());
model.setContact(form.getContact());
@ -259,7 +289,9 @@ public class AffiliationService {
}).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()))
.call(userId -> keycloakService.setAutoRoleMembre(userId, m.getRole(), m.getGrade_arbitrage()))
.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) ?
Uni.createFrom().nullItem() :
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(),
licenceModel))));
}
public Uni<?> accept(AffiliationRequestSaveForm form) {
LOGGER.debug("Affiliation Request Accepted");
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.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))
.call(club -> setMembre(form.new Member(1), club, req.getSaison())
.call(__ -> setMembre(form.new Member(2), club, req.getSaison())
.call(club -> setMembre(form.new Member(1), club, req.getSaison()).onFailure()
.recoverWithNull()
.call(__ -> setMembre(form.new Member(2), club, req.getSaison()).onFailure()
.recoverWithNull()
.call(___ -> setMembre(form.new Member(3), club, req.getSaison()))))
.onItem()
.invoke(model -> Uni.createFrom()
@ -298,13 +335,13 @@ public class AffiliationService {
}
private Uni<ClubModel> acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) {
LOGGER.debug("New Club Accepted");
return Uni.createFrom().nullItem()
.chain(() -> {
ClubModel club = new ClubModel();
club.setName(form.getName());
club.setCountry("FR");
club.setSIRET(form.getSiret());
club.setRNA(form.getRna());
club.setStateId(form.getState_id());
club.setAddress(form.getAddress());
club.setContact_intern(form.getContact());
club.setAffiliations(new ArrayList<>());
@ -336,17 +373,24 @@ public class AffiliationService {
}
private Uni<ClubModel> acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) {
AtomicBoolean nameChange = new AtomicBoolean(false);
LOGGER.debug("Old Club Accepted");
return Uni.createFrom().nullItem()
.chain(() -> {
club.setName(form.getName());
if (!form.getName().equals(club.getName())) {
club.setName(form.getName());
nameChange.set(true);
}
club.setCountry("FR");
club.setSIRET(form.getSiret());
club.setRNA(form.getRna());
club.setStateId(form.getState_id());
club.setAddress(form.getAddress());
club.setContact_intern(form.getContact());
return Panache.withTransaction(() -> clubRepository.persist(club)
.chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
.chain(() -> repositoryRequest.delete(model)));
.chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
.chain(() -> repositoryRequest.delete(model)))
.call(() -> nameChange.get() ? keycloakService.updateGroupFromClub(
club) // update group in keycloak
: Uni.createFrom().nullItem());
})
.map(__ -> club);
}
@ -354,7 +398,7 @@ public class AffiliationService {
public Uni<SimpleReqAffiliation> getRequest(long id) {
return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel)
.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) {
out.setClub(c.getId());
out.setClub_name(c.getName());
@ -367,7 +411,7 @@ public class AffiliationService {
public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliation() {
return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison())
.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())
.chain(aff -> repository.list("saison = ?1", Utils.getSaison())
.map(models -> models.stream().map(SimpleAffiliation::fromModel).toList())
@ -379,9 +423,9 @@ public class AffiliationService {
return clubRepository.findById(id)
.onItem().ifNull().failWith(new DNotFoundException("Club non trouvé"))
.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 ->
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,
model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList())
);
@ -411,9 +455,9 @@ public class AffiliationService {
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)
.call(aff -> reactiveMailer.send(
.call(aff -> federationAdmin ? reactiveMailer.send(
Mail.withText(aff.getM1_email(),
"FFSAF - Votre demande d'affiliation a été rejetée.",
String.format(
@ -430,7 +474,7 @@ public class AffiliationService {
""", aff.getName(), reason)
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(aff.getM2_email(), aff.getM3_email())
))
) : Uni.createFrom().nullItem())
.chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))
.call(__ -> Utils.deleteMedia(id, media, "aff_request/logo"))
.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 java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@ -193,12 +194,17 @@ public class ClubService {
}
public Uni<String> update(long id, FullClubForm input) {
AtomicBoolean nameChange = new AtomicBoolean(false);
return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()))
.onItem().transformToUni(Unchecked.function(m -> {
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.setInternational(input.isInternational());
@ -211,11 +217,9 @@ public class ClubService {
m.setTraining_day_time(input.getTraining_day_time());
ls.logChange("Contact interne", m.getContact_intern(), input.getContact_intern(), m);
m.setContact_intern(input.getContact_intern());
ls.logChange("N° RNA", m.getRNA(), input.getRna(), m);
m.setRNA(input.getRna());
if (input.getSiret() != null && !input.getSiret().isBlank()) {
ls.logChange("N° SIRET", m.getSIRET(), input.getSiret(), m);
m.setSIRET(Long.parseLong(input.getSiret()));
if (input.getState_id() != null && !input.getState_id().isBlank()) {
ls.logChange("N° SIRET", m.getClubId(), input.getState_id(), m);
m.setStateId(input.getState_id());
}
ls.logChange("Adresse administrative", m.getAddress(), input.getAddress(), m);
m.setAddress(input.getAddress());
@ -230,6 +234,8 @@ public class ClubService {
}
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,
SimpleClubModel.fromModel(membreModel)))
.map(__ -> "OK");
@ -251,9 +257,8 @@ public class ClubService {
clubModel.setTraining_location(input.getTraining_location());
clubModel.setTraining_day_time(input.getTraining_day_time());
clubModel.setContact_intern(input.getContact_intern());
clubModel.setRNA(input.getRna());
if (input.getSiret() != null && !input.getSiret().isBlank())
clubModel.setSIRET(Long.parseLong(input.getSiret()));
if (input.getState_id() != null && !input.getState_id().isBlank())
clubModel.setStateId(input.getState_id());
clubModel.setAddress(input.getAddress());
try {
@ -300,9 +305,9 @@ public class ClubService {
.call(clubModel -> Mutiny.fetch(clubModel.getAffiliations()))
.invoke(clubModel -> {
data.setName(clubModel.getName());
data.setSiret(clubModel.getSIRET());
data.setRna(clubModel.getRNA());
data.setState_id(clubModel.getStateId());
data.setAddress(clubModel.getAddress());
data.setContact(clubModel.getContact_intern());
data.setSaison(
clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison))
.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);
}
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) {
if (membreModel.getUserId() == null) {
return Uni.createFrom()
@ -199,16 +224,16 @@ public class KeycloakService {
public Uni<String> initCompte(long id) {
return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> {
if (membreModel.getUserId() != null)
throw new KeycloakException("User already linked to the user id=" + id);
if (membreModel.getEmail() == null)
throw new KeycloakException("User email is null");
if (membreModel.getFname() == null || membreModel.getLname() == null)
throw new KeycloakException("User name is null");
})).chain(membreModel -> creatUser(membreModel).chain(user -> {
LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId());
}));
if (membreModel.getUserId() != null)
throw new KeycloakException("User already linked to the user id=" + id);
if (membreModel.getEmail() == null)
throw new KeycloakException("User email is null");
if (membreModel.getFname() == null || membreModel.getLname() == null)
throw new KeycloakException("User name is null");
})).chain(membreModel -> creatUser(membreModel).chain(user -> {
LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId());
}));
}
private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
@ -231,9 +256,6 @@ public class KeycloakService {
user.setEmail(membreModel.getEmail());
user.setEnabled(true);
user.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name(),
RequiredAction.UPDATE_PASSWORD.name()));
try (Response response = keycloak.realm(realm).users().create(user)) {
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo()
.equals(Response.Status.CONFLICT))
@ -245,13 +267,6 @@ public class KeycloakService {
return getUser(login).orElseThrow(
() -> 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()))
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
.call(user -> enabled_email ? reactiveMailer.send(
@ -261,14 +276,14 @@ public class KeycloakService {
"""
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éé.
Ce compte vous permettra de consulter vos informations, de vous inscrire aux compétitions et de consulter vos 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.
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 et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats.
L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
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.
(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.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.function.Consumer;
@ -26,6 +27,7 @@ import java.util.function.Function;
@WithSession
@ApplicationScoped
public class LicenceService {
private static final Logger LOGGER = Logger.getLogger(LicenceService.class);
@Inject
LicenceRepository repository;
@ -125,7 +127,9 @@ public class LicenceService {
.chain(() -> combRepository.persist(membreModel))
: Uni.createFrom().nullItem())
.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());
}

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.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
@ -102,15 +103,47 @@ public class MembreService {
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,
int licenceRequest, int payState) {
int licenceRequest, int payState, String order, String categorie) {
if (search == null)
search = "";
search = "%" + search.replaceAll(" ", "% %") + "%";
String categorieFilter;
if (categorie == null || categorie.isBlank())
categorieFilter = " True";
else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
String finalSearch = search;
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
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> {
@ -120,18 +153,18 @@ public class MembreService {
if (club == null || club.isBlank()) {
query = repository.find(
"id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")",
Sort.ascending("fname", "lname"), finalSearch, ids)
"id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, ids)
.page(Page.ofSize(limit));
} else {
if (club.equals("null")) {
query = repository.find(
"id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ")",
Sort.ascending("fname", "lname"), finalSearch, ids).page(Page.ofSize(limit));
"id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, ids).page(Page.ofSize(limit));
} else {
query = repository.find(
"id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")",
Sort.ascending("fname", "lname"), finalSearch, club + "%", ids)
"id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, club, ids)
.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,
String subject) {
String order, String categorie, String subject) {
if (search == null)
search = "";
search = "%" + search.replaceAll(" ", "% %") + "%";
@ -149,6 +182,16 @@ public class MembreService {
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
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> {
@ -157,8 +200,8 @@ public class MembreService {
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> {
PanacheQuery<MembreModel> query = repository.find(
"id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")",
Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub(), ids)
"id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, membreModel.getClub(), ids)
.page(Page.ofSize(limit));
return getPageResult(query, limit, page);
});
@ -197,6 +240,11 @@ public class MembreService {
return Uni.createFrom().nullItem();
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()
.chain(membreModel -> {
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",
data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).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 -> {
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(
"Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club");
}
}
Uni<Void> uniResult = Uni.createFrom().voidItem();
for (SimpleMembreInOutData dataIn : data2) {
MembreModel model = membres.stream()
.filter(m -> Objects.equals(m.getLicence(), dataIn.getLicence()) || m.getLname()
.equals(dataIn.getNom()) && m.getFname().equals(dataIn.getPrenom()) ||
Objects.equals(m.getFname(), dataIn.getEmail())).findFirst()
.filter(m -> (dataIn.getLicence() != null && Objects.equals(m.getLicence(),
dataIn.getLicence())) || m.getLname().equals(dataIn.getNom()) && m.getFname()
.equals(dataIn.getPrenom()) || (dataIn.getEmail() != null && !dataIn.getEmail()
.isBlank() && Objects.equals(m.getFname(), dataIn.getEmail()))).findFirst()
.orElseGet(() -> {
MembreModel mm = new MembreModel();
mm.setClub(clubModel.get());
@ -226,16 +278,23 @@ public class MembreService {
mm.setCountry("FR");
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())) {
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(),
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
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(),
dataIn.getNom().toUpperCase()) > 3) || (!add && StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException(
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide.");
}
@ -319,7 +379,7 @@ public class MembreService {
return update(repository.findById(id)
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> {
if (c > 0)
if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser");
})))
.chain(membreModel -> clubRepository.findById(membre.getClub())
@ -341,7 +401,7 @@ public class MembreService {
return update(repository.findById(id)
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> {
if (c > 0)
if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser");
})))
.invoke(Unchecked.consumer(membreModel -> {

View File

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

View File

@ -1,7 +1,8 @@
package fr.titionfire.ffsaf.rest;
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 io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*;
@ -12,18 +13,24 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("api/asso")
public class AssoEndpoints {
@RestClient
StateIdService stateIdService;
@RestClient
SirenService sirenService;
@GET
@Path("siren/{siren}")
@Path("state_id/{stateId}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<UniteLegaleRoot> getInfoSiren(@PathParam("siren") String siren) {
return sirenService.get_unite(siren).onFailure().transform(throwable -> {
public Uni<AssoData> getAssoInfo(@PathParam("stateId") String stateId) {
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 (exception.getResponse().getStatus() == 404)
return new DNotFoundException("Service momentanément indisponible");
if (exception.getResponse().getStatus() == 400)
return new DNotFoundException("Siret introuvable");
return new DNotFoundException("Asso introuvable");
}
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 java.net.URISyntaxException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
@ -69,7 +70,8 @@ public class ClubEndpoints {
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
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

View File

@ -58,13 +58,15 @@ public class MembreAdminEndpoints {
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
@Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@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 = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) {
@Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie,
@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)
limit = 50;
if (page == null || 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

View File

@ -50,13 +50,15 @@ public class MembreClubEndpoints {
@Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit,
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
@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 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)
limit = 50;
if (page == null || 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

View File

@ -1,6 +1,7 @@
package fr.titionfire.ffsaf.rest.client;
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;
@ -15,5 +16,6 @@ public interface SirenService {
@GET
@Path("/v3/unites_legales/{SIREN}")
@CacheResult(cacheName = "AssoData_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
public class RenewAffData {
String name;
Long siret;
String rna;
String state_id;
String address;
String contact;
int saison;
List<RenewMember> members;

View File

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

View File

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

View File

@ -20,8 +20,8 @@ public class SimpleClubList {
String name;
@Schema(description = "Pays du club", example = "FR")
String country;
@Schema(description = "Numéro SIRET du club", example = "12345678901234")
Long siret;
@Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234")
String state_id;
@Schema(description = "Numéro d'affiliation du club", example = "12345")
Long no_affiliation;
@ -29,7 +29,7 @@ public class SimpleClubList {
if (model == 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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@ notif.affRequest.mail=
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.StateIdService".url=https://www.data-asso.fr/api/
#Login
quarkus.oidc.token-state-manager.split-tokens=true

View File

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

View File

@ -1,11 +1,11 @@
import {useEffect, useRef} from 'react'
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 {AdminRoot, getAdminChildren} from "./pages/admin/AdminRoot.jsx";
import {AuthCallback} from "./components/auhCallback.jsx";
import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx";
import {check_validity} from "./utils/auth.js";
import {KeycloakContextProvider, useAuth, useAuthDispatch} from "./hooks/useAuth.jsx";
import {check_validity, login} from "./utils/auth.js";
import {ToastContainer} from "react-toastify";
import './App.css'
@ -14,6 +14,7 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {FallingLines} from "react-loader-spinner";
const router = createBrowserRouter([
{
@ -113,6 +114,43 @@ function Root() {
theme="light"
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>
</>
}

View File

@ -38,7 +38,7 @@ export function HoraireEditor({data}) {
return <div className="row mb-3">
<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">
{state.map((d, index) => {
return <div key={index} className="input-group">
@ -92,4 +92,4 @@ export function HoraireEditor({data}) {
</div>
</ul>
</div>
}
}

View File

@ -42,7 +42,7 @@ export function LocationEditor({data, setModal, sendData}) {
return <div className="row mb-3">
<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">
{state.map((d, index) => {
return <div key={index} className="input-group">

View File

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

View File

@ -54,7 +54,7 @@ function ClubMenu() {
</div>
<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/member">Member</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Membres</NavLink></li>
</ul>
</li>
}
@ -70,7 +70,7 @@ function AdminMenu() {
Administration
</div>
<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/stats">Statistiques</NavLink></li>
</ul>
@ -99,4 +99,4 @@ function LoginMenu() {
</li>
}
</>
}
}

View File

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

View File

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

View File

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

View File

@ -6,38 +6,60 @@ import {useEffect, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import {Checkbox} from "../components/MemberCustomFiels.jsx";
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 {SearchBar} from "../components/SearchBar.jsx";
import * as XLSX from "xlsx-js-style";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign} from "@fortawesome/free-solid-svg-icons";
let lastRefresh = "";
export function MemberList({source}) {
const {hash} = useLocation();
const navigate = useNavigate();
let page = Number(hash.substring(1));
page = (page > 0) ? page : 1;
const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]);
const [showLicenceState, setShowLicenceState] = useState(false);
const [clubFilter, setClubFilter] = useState("");
const [stateFilter, setStateFilter] = useState(4)
const [lastSearch, setLastSearch] = useState("");
const [paymentFilter, setPaymentFilter] = useState(2);
const setFilter = (filter) => {
navigate("#" + encodeURI(JSON.stringify(filter)))
}
const filter = {
page: 1,
search: "",
club: "",
licenceRequest: 4,
payment: 2,
order: "",
categorie: "",
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
}
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(() => {
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
}, [hash, clubFilter, stateFilter, lastSearch, 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}`;
if (tmp === lastRefresh)
return;
lastRefresh = tmp
refresh(lastRefresh);
}, [hash]);
useEffect(() => {
if (!data)
return;
if (data.page_count < filter.page) {
setFilter({...filter, page: 1});
}
const data2 = [];
for (const e of data.result) {
data2.push({
@ -74,19 +96,19 @@ export function MemberList({source}) {
}, [showLicenceState]);
const search = (search) => {
if (search === lastSearch)
if (search === filter.search)
return;
setLastSearch(search);
setFilter({...filter, search: search});
}
return <>
<div>
<div className="row">
<div className="col-lg-9">
<SearchBar search={search}/>
<SearchBar search={search} defaultValue={filter.search}/>
{data
? <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
? <AxiosError error={error}/>
: <Def/>
@ -102,13 +124,28 @@ export function MemberList({source}) {
<button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des
licences</button>}
</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-header">Filtre</div>
<div className="card-body">
<FiltreBar showLicenceState={showLicenceState} setShowLicenceState={setShowLicenceState} data={data}
clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
setPaymentFilter={setPaymentFilter}/>
<FiltreBar showLicenceState={showLicenceState}
setShowLicenceState={setShowLicenceState}
clubFilter={filter.club}
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>
@ -169,6 +206,10 @@ function FileOutput() {
certifDate: e.certif ? new Date(e.certif.split("¤")[1]) : '',
}
if (isNaN(tmp.certifDate) || tmp.certifDate === 'NaN') {
tmp.certifDate = ''
}
//tmp.birthdate.setMilliseconds(0);
//tmp.birthdate.setSeconds(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 = []
for (let i = 1; i <= data.page_count; i++) {
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>);
}
@ -353,10 +394,10 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
<nav aria-label="Page navigation">
<ul className="pagination justify-content-center">
<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}
<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>
</nav>
</div>
@ -365,45 +406,146 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
function MakeRow({member, showLicenceState, navigate, source}) {
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') : "-------") + " "}
{(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="fw-bold">{member.fname} {member.lname}</div>
</div>
</div>
{source === "club" ?
<small>{member.categorie}</small>
: <small>{member.club?.name || "Sans club"}</small>}
<div style={{verticalAlign: "center", margin: "auto 0"}}>
{source === "club" ?
<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) {
return <div
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"))}
onClick={() => navigate("" + member.id)}>{rowContent}</div>
} else {
return <div className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
onClick={() => navigate("" + member.id)}>
return <a 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"))}
style={{padding: "0 1em"}}
onClick={e => {
e.preventDefault();
navigate("" + member.id)
}}
href={"member/" + member.id}>
{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}) {
useEffect(() => {
if (!data)
return;
allClub.push(...data.result.map((e) => e.club?.name))
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
}, [data]);
const handleChange = (index, value) => {
const newCriteria = [...orderCriteria];
newCriteria[index] = value;
// Si le dernier critère est rempli, on en ajoute un nouveau
if (index === orderCriteria.length - 1 && value !== '') {
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>
<div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
</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}/>}
<div className="mb-3">
<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="fw-bold">{request.name}</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>
}
@ -104,4 +104,4 @@ function Def() {
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
</div>
}
}

View File

@ -8,7 +8,6 @@ import {RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {useEffect, useRef, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
const vite_url = import.meta.env.VITE_URL;
@ -67,8 +66,7 @@ function Content({data, refresh}) {
formData.append('id', data.id);
formData.append('name', event.target.name.value);
formData.append('siret', event.target.siret.value);
formData.append('rna', event.target.rna.value);
formData.append('state_id', event.target.state_id.value);
formData.append('address', event.target.address.value);
formData.append('contact', event.target.contact.value);
@ -166,7 +164,7 @@ function Content({data, refresh}) {
<input name="id" value={data.id} readOnly hidden/>
<div className="card-header">Demande d'affiliation</div>
<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>
<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>}
</div>
<TextField type="number" name="siret" text="SIRET" value={data.siret} disabled={true}/>
<TextField name="rna" text="RNA" value={data.rna} required={false}/>
<TextField name="state_id" text="SIRET ou RNA" value={data.stateId} disabled={true}/>
<TextField name="address" text="Adresse" value={data.address}/>
<TextField name="contact" text="Contact administratif" value={data.contact}/>

View File

@ -40,7 +40,7 @@ export function ClubList() {
country: e.country,
siret: e.siret,
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);
@ -197,4 +197,4 @@ function Def() {
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
</div>
}
}

View File

@ -130,8 +130,7 @@ function InformationForm({data}) {
</div>
</div>
{!switchOn && <>
<TextField name="siret" text="SIRET" value={data.siret} required={false} type="number"/>
<TextField name="rna" text="RNA" value={data.rna} required={false}/>
<TextField name="state_id" text="SIRET ou RNA" value={data.state_id} required={false}/>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}
placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" value={data.address} required={false}
@ -172,6 +171,7 @@ function InformationForm({data}) {
export function BureauCard({clubData}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1)
const navigate = useNavigate();
return <>
<div className="card mb-4">
@ -179,7 +179,8 @@ export function BureauCard({clubData}) {
<div className="card-body">
<ul className="list-group">
{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>
})}
@ -188,4 +189,4 @@ export function BureauCard({clubData}) {
</div>
{error && <AxiosError error={error}/>}
</>
}
}

View File

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

View File

@ -34,13 +34,13 @@ export function MemberPage() {
}
}
).then(_ => {
navigate("/admin/member")
navigate(-1)
})
}
return <>
<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
</button>
{data

View File

@ -23,7 +23,7 @@ export function ClubRoot() {
return <>
<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>
<Outlet/>
</LoadingProvider>

View File

@ -1,12 +1,11 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {useEffect, useReducer, useState} from "react";
import {useState} from "react";
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 {apiAxios, getSaison} from "../../../utils/Tools.js";
import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
import {useNavigate} from "react-router-dom";
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='#'>
<button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}}
onClick={e => null}>
Téléchargée l'attestation d'affiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
onClick={_ => null}>
Télécharger lattestation daffiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button>
</a>
</div>
@ -140,10 +139,10 @@ function ModalContent2({clubData, data}) {
}
}
if (list.length !== 3) {
toast.error("Il faut sélectionner 3 membres pour renouveler l'affiliation")
return
while (list.length < 3) {
list.push(-1)
}
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)))
})

View File

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

View File

@ -49,7 +49,9 @@ export function InformationForm({data}) {
<TextField name="lname" text="Nom" value={data.lname}/>
<TextField name="fname" text="Prénom" value={data.fname}/>
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
type="email"/>
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}
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<CountryList name="country" text="Pays" value={data.country}/>

View File

@ -33,13 +33,13 @@ export function MemberPage() {
}
}
).then(_ => {
navigate("/club/member")
navigate(-1)
})
}
return <>
<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
</button>
{data

View File

@ -23,6 +23,20 @@ export const errFormater = (data, msg) => {
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()) {
const currentSaison = getSaison(currentDate)
const birthYear = birth_date.getFullYear()
@ -60,3 +74,32 @@ export function getSaison(currentDate = new Date()) {
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 => {
online_callback({state: true, userinfo: data.data});
})
}else{
online_callback({state: false});
}
}).catch(() => {
console.log("=> Not authenticated");
online_callback({state: false});
})
}
@ -32,4 +35,4 @@ export function login_redirect() {
export function logout() {
window.location.href = `${vite_url}/api/logout`;
}
}

View File

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