74 Commits

Author SHA1 Message Date
d3fb884ab4 Merge pull request 'fix: Me page' (#75) from dev into master
Reviewed-on: #75
2025-12-26 13:16:56 +00:00
81faafe2de fix: Me page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m46s
2025-12-26 14:16:30 +01:00
ee730be570 Merge pull request 'dev' (#74) from dev into master
Reviewed-on: #74
2025-12-25 19:03:17 +00:00
4856326b29 feat: html mail for all public
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 11m4s
2025-12-25 19:59:47 +01:00
b94f4b43ce fix: reword message + fix missing comb un cat list (cma) 2025-12-25 19:27:15 +01:00
795d044c0b fix:typo 2025-12-25 19:25:56 +01:00
dc5ab9c3c4 feat: add inactif member filter 2025-12-20 18:22:24 +01:00
870891a77c Merge pull request 'dev' (#73) from dev into master
Reviewed-on: #73
2025-12-19 15:14:04 +00:00
7e1356b621 fix: cm auto ask new category on empty competition
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m17s
2025-12-19 16:13:37 +01:00
b689521e56 fix: comb register with null license 2025-12-19 16:02:28 +01:00
e81f475402 Merge pull request 'dev-comp' (#72) from dev into master
Reviewed-on: #72
2025-12-19 13:47:40 +00:00
095a5fcede Merge branch 'dev-comp' into dev
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m20s
2025-12-19 14:43:46 +01:00
3decc6d0e0 Merge pull request 'dev' (#71) from dev into master
Reviewed-on: #71
2025-12-18 15:37:19 +00:00
254fd7d73f feat: html email
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m5s
2025-12-18 16:36:30 +01:00
12a75d003c feat: mask inactive club on map 2025-12-18 15:33:45 +01:00
7f957b98f2 fix: deleteLicence Panache missing transaction context 2025-12-18 15:24:32 +01:00
8b2c9613fd Merge pull request 'dev' (#70) from dev into master
Reviewed-on: #70
2025-12-18 14:03:51 +00:00
50b49c40e3 fix: error 500 when license is null
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m6s
2025-12-18 15:03:22 +01:00
47d3921e05 fix: email typo 2025-12-18 14:57:26 +01:00
ecc9753237 Merge pull request 'feat: add cat filter on mass license validation' (#69) from dev into master
Reviewed-on: #69
2025-12-18 13:47:32 +00:00
7c4addd525 feat: add cat filter on mass license validation
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-12-18 14:46:52 +01:00
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
25 changed files with 377 additions and 197 deletions

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 ## Running the application in dev mode
@@ -52,34 +66,3 @@ 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` 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. 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)

View File

@@ -355,10 +355,16 @@ public class AffiliationService {
.map(c -> club)); .map(c -> club));
}) })
.call(club -> reactiveMailer.send( .call(club -> reactiveMailer.send(
Mail.withText(form.getM1_email(), Mail.withHtml(form.getM1_email(), "FFSAF - Creation de votre compte sur l'intranet",
"FFSAF - Acceptation de votre demande d'affiliation", String.format(Utils.HTML_HEADER, "FFSAF - Creation de votre compte sur l'intranet") +
String.format( String.format("""
""" <p>Votre demande d'affiliation pour le club <span class="highlight">%s</span> a été acceptée.</p>
<p>LLe numéro d'affiliation de votre club est le <span class="highlight">%d</span>.</p>
""",
club.getName(), club.getNo_affiliation()) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour, Bonjour,
Votre demande d'affiliation pour le club %s a été acceptée. Votre demande d'affiliation pour le club %s a été acceptée.
@@ -366,8 +372,8 @@ public class AffiliationService {
Cordialement, Cordialement,
L'équipe de la FFSAF L'équipe de la FFSAF
""", club.getName(), club.getNo_affiliation()) """, club.getName(), club.getNo_affiliation()))
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr") .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(form.getM2_email(), form.getM3_email()) .addTo(form.getM2_email(), form.getM3_email())
)); ));
} }
@@ -458,10 +464,16 @@ public class AffiliationService {
public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) { public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) {
return repositoryRequest.findById(id) return repositoryRequest.findById(id)
.call(aff -> federationAdmin ? reactiveMailer.send( .call(aff -> federationAdmin ? reactiveMailer.send(
Mail.withText(aff.getM1_email(), Mail.withHtml(aff.getM1_email(), "FFSAF - Votre demande d'affiliation a été rejetée.",
"FFSAF - Votre demande d'affiliation a été rejetée.", String.format(Utils.HTML_HEADER, "FFSAF - Votre demande d'affiliation a été rejetée.") +
String.format( String.format("""
""" <p>Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):<br/>%s</p>
<p>Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@ffsaf.fr">contact@ffsaf.fr</a>.</p>
""",
aff.getName(), reason) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour, Bonjour,
Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s): Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
@@ -471,8 +483,8 @@ public class AffiliationService {
Cordialement, Cordialement,
L'équipe de la FFSAF L'équipe de la FFSAF
""", aff.getName(), reason) """, aff.getName(), reason))
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr") .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(aff.getM2_email(), aff.getM3_email()) .addTo(aff.getM2_email(), aff.getM3_email())
) : Uni.createFrom().nullItem()) ) : Uni.createFrom().nullItem())
.chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff))) .chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import fr.titionfire.ffsaf.data.model.AffiliationModel; import fr.titionfire.ffsaf.data.model.AffiliationModel;
import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.AffiliationRepository;
import fr.titionfire.ffsaf.data.repository.ClubRepository; import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.net2.ServerCustom; import fr.titionfire.ffsaf.net2.ServerCustom;
@@ -44,6 +45,9 @@ public class ClubService {
@Inject @Inject
ClubRepository repository; ClubRepository repository;
@Inject
AffiliationRepository affiliationRepository;
@Inject @Inject
ServerCustom serverCustom; ServerCustom serverCustom;
@@ -234,7 +238,8 @@ public class ClubService {
} }
return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append()); return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
})) }))
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(clubModel) // update group in keycloak .call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(
clubModel) // update group in keycloak
: Uni.createFrom().nullItem()) : Uni.createFrom().nullItem())
.invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients, .invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
SimpleClubModel.fromModel(membreModel))) SimpleClubModel.fromModel(membreModel)))
@@ -323,9 +328,11 @@ public class ClubService {
} }
public Uni<List<ClubMapData>> getMapData() { public Uni<List<ClubMapData>> getMapData() {
return repository.list("international", false).toMulti().flatMap(list -> Multi.createFrom().iterable(list)) return affiliationRepository.list("saison >= ?1 AND club.international = ?2", Utils.getSaison() - 1, false)
.call(clubModel -> Mutiny.fetch(clubModel.getContact())) .toMulti().flatMap(list -> Multi.createFrom().iterable(list))
.map(clubModel -> { .call(affiliationModel -> Mutiny.fetch(affiliationModel.getClub().getContact()))
.map(affiliationModel -> {
ClubModel clubModel = affiliationModel.getClub();
ClubMapData data = new ClubMapData(); ClubMapData data = new ClubMapData();
data.setName(clubModel.getName()); data.setName(clubModel.getName());

View File

@@ -281,7 +281,7 @@ public class CompetitionService {
public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data, public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data,
String source) { String source) {
if ("admin".equals(source)) if ("admin".equals(source))
if (data.getLicence() != -1) { // not a guest if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
return permService.hasEditPerm(securityCtx, id) return permService.hasEditPerm(securityCtx, id)
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences())) .call(combModel -> Mutiny.fetch(combModel.getLicences()))

View File

@@ -156,10 +156,18 @@ public class KeycloakService {
return oldEmail; return oldEmail;
}).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") : }).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") :
reactiveMailer.send( reactiveMailer.send(
Mail.withText(oldEmail, Mail.withHtml(oldEmail, "FFSAF - Changement de votre adresse email",
"FFSAF - Changement de votre adresse email", String.format(Utils.HTML_HEADER, "FFSAF - Changement de votre adresse email") +
String.format( String.format("""
""" <p>Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,<br/>
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.</p>
<p>Ancienne adresse email : <span class="highlight">%s</span><br/>Nouvelle adresse email : <span class="highlight">%s</span></p>
<p>Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
""",
oldEmail, email) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour, Bonjour,
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF, Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
@@ -172,8 +180,8 @@ public class KeycloakService {
Cordialement, Cordialement,
L'équipe de la FFSAF L'équipe de la FFSAF
""", oldEmail, email) """, oldEmail, email))
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr") .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e))); ).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
} }
@@ -270,13 +278,33 @@ public class KeycloakService {
.invoke(user -> membreModel.setUserId(user.getId())) .invoke(user -> membreModel.setUserId(user.getId()))
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of())) .call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
.call(user -> enabled_email ? reactiveMailer.send( .call(user -> enabled_email ? reactiveMailer.send(
Mail.withText(user.getEmail(), Mail.withHtml(user.getEmail(), "FFSAF - Creation de votre compte sur l'intranet",
"FFSAF - Creation de votre compte sur l'intranet", String.format(Utils.HTML_HEADER, "Création de votre compte intranet FFSAF") +
String.format( String.format(
"""
<p>Suite &agrave; votre premi&egrave;re inscription <span class="highlight">%s</span> &agrave; la <strong>F&eacute;d&eacute;ration France Soft Armored Fighting (FFSAF)</strong>, votre compte intranet a &eacute;t&eacute; cr&eacute;&eacute;.</p>
<p>Ce compte vous permettra de :</p>
<ul>
<li>Consulter vos informations personnelles,</li>
<li>Vous inscrire aux comp&eacute;titions (bient&ocirc;t disponible),</li>
<li>Consulter les r&eacute;sultats des comp&eacute;titions.</li>
</ul>
<p>L&rsquo;intranet est accessible &agrave; l&rsquo;adresse suivante :</p>
<p style="text-align: center;"><a href="https://intra.ffsaf.fr" class="button">Acc&eacute;der &agrave; l&apos;intranet</a></p>
<p>Votre nom d&rsquo;utilisateur est : <span class="highlight">%s</span></p>
<p>Pour d&eacute;finir votre mot de passe, rendez-vous sur l&rsquo;intranet : <strong>Connexion &gt; Mot de passe oubli&eacute; ?</strong></p>
<p>Si vous n&rsquo;avez pas demand&eacute; cette inscription, veuillez contacter le support &agrave; l&rsquo;adresse : <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
<p class="footer">(Pas de panique, nous ne vous enverrons pas de message autre que ceux concernant votre compte.)</p>
""",
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername()) +
Utils.HTML_FOOTER
)
.setText(String.format(
""" """
Bonjour, Bonjour,
Suite à votre première inscription %sà la Fédération Française de Soft Armored Fighting (FFSAF), votre compte intranet a été créé. Suite à votre première inscription %sà la Fédération France 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. 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 L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
@@ -291,8 +319,8 @@ public class KeycloakService {
L'équipe de la FFSAF L'équipe de la FFSAF
""", """,
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub() membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername()) .getName() + ") " : "", user.getUsername()))
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr") .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
) : Uni.createFrom().nullItem()) ) : Uni.createFrom().nullItem())
.call(user -> membreService.setUserId(membreModel.getId(), user.getId())) .call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub())); .call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));

View File

@@ -84,6 +84,26 @@ public class LicenceService {
)); ));
} }
public Uni<?> validePaymentLicences(List<Long> ids) {
Uni<String> uni = Uni.createFrom().nullItem();
for (Long id : ids) {
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
.chain(model -> {
if (!model.isPay())
ls.logUpdate("payment (admin) de la licence", model);
return validatePayLicences(model);
}))
.map(__ -> "OK");
}
return uni.call(__ -> ls.append());
}
protected Uni<LicenceModel> validatePayLicences(LicenceModel model) {
model.setPay(true);
return Panache.withTransaction(() -> repository.persist(model));
}
public Uni<LicenceModel> setLicence(long id, LicenceForm form) { public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
if (form.getId() == -1) { if (form.getId() == -1) {
return combRepository.findById(id).chain(membreModel -> { return combRepository.findById(id).chain(membreModel -> {
@@ -121,7 +141,7 @@ public class LicenceService {
} }
private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() { private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() {
return membreModel -> ((membreModel.getLicence() <= 0) ? return membreModel -> ((membreModel.getLicence() == null || membreModel.getLicence() <= 0) ?
sequenceRepository.getNextValueInTransaction(SequenceType.Licence) sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
.invoke(i -> membreModel.setLicence(Math.toIntExact(i))) .invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
.chain(() -> combRepository.persist(membreModel)) .chain(() -> combRepository.persist(membreModel))
@@ -157,7 +177,7 @@ public class LicenceService {
"Impossible de supprimer une licence pour laquelle un paiement est en cours"); "Impossible de supprimer une licence pour laquelle un paiement est en cours");
}))) })))
.call(model -> ls.logADelete(model)) .call(model -> ls.logADelete(model))
.chain(model -> repository.delete(model)); .chain(model -> Panache.withTransaction(() -> repository.delete(model)));
} }
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) { public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) {

View File

@@ -125,8 +125,21 @@ public class MembreService {
return sort; return sort;
} }
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club, public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
int licenceRequest, int payState, String order, String categorie) { String order, String categorie, Boolean archive, String subject) {
return repository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(membreModel -> {
if (membreModel == null || membreModel.getClub() == null)
throw new DForbiddenException();
}))
.chain(membreModel ->
searchAdmin(limit, page, search, membreModel.getClub(), licenceRequest, payState, order,
categorie, archive));
}
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, Object club,
int licenceRequest, int payState, String order, String categorie,
Boolean archive) {
if (search == null) if (search == null)
search = ""; search = "";
search = "%" + search.replaceAll(" ", "% %") + "%"; search = "%" + search.replaceAll(" ", "% %") + "%";
@@ -137,77 +150,47 @@ public class MembreService {
else else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal(); categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
String finalSearch = search; String archiveFilter;
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState); if (archive == null || archive)
archiveFilter = "True";
else
archiveFilter = String.format(
"EXISTS (SELECT 1 FROM LicenceModel l WHERE l.membre.id = m.id AND l.saison >= %s)",
Utils.getSaison() - 1);
String clubFilter = "?3 = ?3";
if (club != null) {
if (club instanceof String club_) {
if (!club_.isBlank()) {
if (club_.equals("null"))
clubFilter = "club IS NULL AND ?3 = ?3";
else
clubFilter = "LOWER(club.name) LIKE LOWER(?3)";
}
} else if (club instanceof ClubModel)
clubFilter = "club = ?3";
}
String queryStr = String.format(
"SELECT m FROM MembreModel m WHERE id %s ?2 AND %s AND (%s) AND %s AND %s",
(licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN", clubFilter, FIND_NAME_REQUEST,
categorieFilter,
archiveFilter);
Sort sort = getSort(order); Sort sort = getSort(order);
if (sort == null) if (sort == null)
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie")); return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
return baseUni String finalSearch = search;
return getLicenceListe(licenceRequest, payState)
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> { .chain(ids -> {
PanacheQuery<MembreModel> query; PanacheQuery<MembreModel> query;
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit));
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
if (club == null || club.isBlank()) {
query = repository.find(
"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 + ") 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 + ") AND " + categorieFilter,
sort, finalSearch, club, ids)
.page(Page.ofSize(limit));
}
}
return getPageResult(query, limit, page); return getPageResult(query, limit, page);
}); });
} }
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
String order, String categorie, String subject) {
if (search == null)
search = "";
search = "%" + search.replaceAll(" ", "% %") + "%";
String finalSearch = search;
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 -> {
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> {
PanacheQuery<MembreModel> query = repository.find(
"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);
});
});
}
private Uni<PageResult<SimpleMembre>> getPageResult(PanacheQuery<MembreModel> query, int limit, int page) { private Uni<PageResult<SimpleMembre>> getPageResult(PanacheQuery<MembreModel> query, int limit, int page) {
return Uni.createFrom().item(new PageResult<SimpleMembre>()) return Uni.createFrom().item(new PageResult<SimpleMembre>())
.invoke(result -> result.setPage(page)) .invoke(result -> result.setPage(page))
@@ -287,14 +270,14 @@ public class MembreService {
if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getEmail() != null && !model.getEmail().isBlank()) {
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
LOGGER.info("Similar membres found: " + model); LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); throw new DBadRequestException("Email '" + model.getEmail() + "' déja utilisé");
} }
if (StringSimilarity.similarity(model.getLname().toUpperCase(), if (StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
LOGGER.info("Similar membres found: " + model); LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); throw new DBadRequestException("Email '" + model.getEmail() + "' déja utilisé");
} }
} }
@@ -380,7 +363,7 @@ public class MembreService {
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank()) if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser"); throw new DBadRequestException("Email déjà utilisé");
}))) })))
.chain(membreModel -> clubRepository.findById(membre.getClub()) .chain(membreModel -> clubRepository.findById(membre.getClub())
.map(club -> new Pair<>(membreModel, club))) .map(club -> new Pair<>(membreModel, club)))
@@ -402,7 +385,7 @@ public class MembreService {
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank()) if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser"); throw new DBadRequestException("Email déjà utilisé");
}))) })))
.invoke(Unchecked.consumer(membreModel -> { .invoke(Unchecked.consumer(membreModel -> {
if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
@@ -492,7 +475,7 @@ public class MembreService {
return clubRepository.findById(input.getClub()) return clubRepository.findById(input.getClub())
.call(__ -> repository.count("email LIKE ?1", input.getEmail()) .call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0) throw new DBadRequestException("Email déjà utiliser"); if (c > 0) throw new DBadRequestException("Email déjà utilisé");
}))) })))
.chain(clubModel -> { .chain(clubModel -> {
MembreModel model = getMembreModel(input, clubModel); MembreModel model = getMembreModel(input, clubModel);
@@ -508,7 +491,7 @@ public class MembreService {
return repository.find("userId = ?1", subject).firstResult() return repository.find("userId = ?1", subject).firstResult()
.call(__ -> repository.count("email LIKE ?1", input.getEmail()) .call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> { .invoke(Unchecked.consumer(c -> {
if (c > 0) throw new DBadRequestException("Email déjà utiliser"); if (c > 0) throw new DBadRequestException("Email déjà utilisé");
}))) })))
.call(membreModel -> .call(membreModel ->
repository.count( repository.count(

View File

@@ -118,7 +118,7 @@ public class LicenceEndpoints {
@RolesAllowed("federation_admin") @RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Validation licence", description = "Valide en masse les licence de l'année en cours (pour les administrateurs)") @Operation(summary = "Validation licence", description = "Valide en masse les licences de l'année en cours (pour les administrateurs)")
@APIResponses(value = { @APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"), @APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"), @APIResponse(responseCode = "403", description = "Accès refusé"),
@@ -128,6 +128,21 @@ public class LicenceEndpoints {
return licenceService.valideLicences(ids); return licenceService.valideLicences(ids);
} }
@POST
@Path("validate-pay")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Validation du payment des licences", description = "Valide en masse le payment des licences de l'année en cours (pour les administrateurs)")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<?> validePaymentLicences(@Parameter(description = "Id des membres a valider") List<Long> ids) {
return licenceService.validePaymentLicences(ids);
}
@DELETE @DELETE
@Path("{id}") @Path("{id}")
@RolesAllowed("federation_admin") @RolesAllowed("federation_admin")

View File

@@ -59,6 +59,7 @@ public class MembreAdminEndpoints {
@Parameter(description = "Text à rechercher") @QueryParam("search") String search, @Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@Parameter(description = "Club à filter") @QueryParam("club") String club, @Parameter(description = "Club à filter") @QueryParam("club") String club,
@Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie, @Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie,
@Parameter(description = "Inclure les archive") @QueryParam("archive") Boolean archive,
@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 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 = "État du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment,
@Parameter(description = "Ordre") @QueryParam("order") String order) { @Parameter(description = "Ordre") @QueryParam("order") String order) {
@@ -66,7 +67,8 @@ public class MembreAdminEndpoints {
limit = 50; limit = 50;
if (page == null || page < 1) if (page == null || page < 1)
page = 1; page = 1;
return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order, categorie); return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order,
categorie, archive);
} }
@GET @GET

View File

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

View File

@@ -46,7 +46,7 @@ public class MeData {
this.id = membreModel.getId(); this.id = membreModel.getId();
this.lname = membreModel.getLname(); this.lname = membreModel.getLname();
this.fname = membreModel.getFname(); this.fname = membreModel.getFname();
this.categorie = membreModel.getCategorie().getName(); this.categorie = membreModel.getCategorie() == null ? "catégorie inconnue" : membreModel.getCategorie().getName();
this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName(); this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName();
this.genre = membreModel.getGenre().str; this.genre = membreModel.getGenre().str;
this.licence = membreModel.getLicence(); this.licence = membreModel.getLicence();

View File

@@ -21,6 +21,79 @@ import java.util.concurrent.Future;
public class Utils { public class Utils {
private static final Logger LOGGER = Logger.getLogger(Utils.class); private static final Logger LOGGER = Logger.getLogger(Utils.class);
public static String HTML_HEADER = """
<!DOCTYPE html>
<html data-lt-installed="true">
<head>
<meta charset="UTF-8">
<title>%s</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #003366;
color: white;
padding: 20px;
text-align: center;
border-radius: 5px 5px 0 0;
}
.content {
padding: 20px;
background-color: #f9f9f9;
border-radius: 0 0 5px 5px;
border: 1px solid #ddd;
border-top: none;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #003366;
color: white !important;
text-decoration: none;
border-radius: 5px;
margin: 15px 0;
}
.footer {
margin-top: 20px;
font-size: 0.9em;
color: #666;
text-align: center;
}
.highlight {
font-weight: bold;
color: #003366;
}
</style>
</head>
<body data-gramm="false" data-lt-tmp-id="lt-957854">
<div class="header">
<div><img src="https://intra.ffsaf.fr/Logo-FFSAF-2023.png" alt="ffsaf" height="128">
<h1>F&eacute;d&eacute;ration France Soft Armored Fighting</h1>
</div>
</div>
<div class="content">
<p>Bonjour,</p>
""";
public static String HTML_FOOTER = """
<p>Cordialement,<br>L&rsquo;&eacute;quipe de la FFSAF</p>
</div>
</body>
</html>
""";
public static int getSaison() { public static int getSaison() {
return getSaison(new Date()); return getSaison(new Date());
} }

View File

@@ -292,9 +292,10 @@ public class RMatch {
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied"); throw new DForbiddenException("Permission denied");
})) }))
.call(cm -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm) .call(cm -> Panache.withTransaction(
() -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm))
.call(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove))) .call(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove)))
.call(cm -> matchRepository.list("id IN ?1 AND category = ?2", .call(cm -> Panache.withSession(() -> matchRepository.list("id IN ?1 AND category = ?2",
Stream.concat(data.matchOrderToUpdate.keySet().stream(), Stream.concat(data.matchOrderToUpdate.keySet().stream(),
data.matchPouleToUpdate.keySet().stream()) data.matchPouleToUpdate.keySet().stream())
.distinct().toList(), cm) .distinct().toList(), cm)
@@ -305,13 +306,14 @@ public class RMatch {
model.setCategory_ord(data.matchOrderToUpdate.get(model.getId())); model.setCategory_ord(data.matchOrderToUpdate.get(model.getId()));
})) }))
.call(mm -> Panache.withTransaction(() -> matchRepository.persist(mm))) .call(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
.invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())) .invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
) )
.chain(categoryModel -> { .chain(categoryModel -> {
Uni<List<MatchModel>> uni = Uni.createFrom().item(new ArrayList<>()); Uni<List<MatchModel>> uni = Uni.createFrom().item(new ArrayList<>());
for (AddMatch match : data.newMatch) for (AddMatch match : data.newMatch)
uni = uni.call(l -> creatMatch(categoryModel, match).invoke(l::add)); uni = uni.call(l -> creatMatch(categoryModel, match).invoke(l::add));
return uni; Uni<List<MatchModel>> finalUni = uni;
return Panache.withSession(() -> finalUni);
} }
) )
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)) .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -14,6 +14,7 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx"; import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx"; import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx"; import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {FallingLines} from "react-loader-spinner";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx"; import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
const router = createBrowserRouter([ const router = createBrowserRouter([

View File

@@ -119,13 +119,13 @@ export function CheckField({name, text, value, row = false}) {
</> </>
} }
export const Checkbox = ({label, value, onChange}) => { export const Checkbox = ({label, value, onChange, name = "checkbox1"}) => {
const handleChange = () => { const handleChange = () => {
onChange(!value); onChange(!value);
}; };
return <div className="form-check"> return <div className="form-check">
<input className="form-check-input" type="checkbox" id="checkbox1" checked={value} onChange={handleChange}/> <input className="form-check-input" type="checkbox" id={name} checked={value} onChange={handleChange}/>
<label className="form-check-label" htmlFor="checkbox1">{label}</label> <label className="form-check-label" htmlFor={name}>{label}</label>
</div> </div>
}; };

View File

@@ -299,7 +299,7 @@ function AssoInfo({initData, needFile}) {
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="status">Statue{needFile && "*"}</label> <label className="input-group-text" htmlFor="status">Statuts{needFile && "*"}</label>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/> <input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/>
</div> </div>
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette {!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette
@@ -313,7 +313,7 @@ function MembreInfo({role, initData}) {
return <> return <>
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôles</label> <label className="input-group-text" htmlFor="inputGroupSelect01">Rôle</label>
<select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)} <select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)}
disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required> disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required>
<option value="0">Sélectionner...</option> <option value="0">Sélectionner...</option>

View File

@@ -35,6 +35,7 @@ export function MemberList({source}) {
payment: 2, payment: 2,
order: "", order: "",
categorie: "", categorie: "",
archived: false,
...JSON.parse(decodeURI(hash.substring(1)) || "{}"), ...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
} }
@@ -43,10 +44,10 @@ export function MemberList({source}) {
data, data,
error, error,
refresh 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) } = 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}&archive=${filter.archived}`, setLoading, 1)
useEffect(() => { useEffect(() => {
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}`; 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}&archive=${filter.archived}`;
if (tmp === lastRefresh) if (tmp === lastRefresh)
return; return;
lastRefresh = tmp lastRefresh = tmp
@@ -137,6 +138,8 @@ export function MemberList({source}) {
<div className="card-body"> <div className="card-body">
<FiltreBar showLicenceState={showLicenceState} <FiltreBar showLicenceState={showLicenceState}
setShowLicenceState={setShowLicenceState} setShowLicenceState={setShowLicenceState}
showArchived={filter.archived}
setShowArchived={e => setFilter({...filter, archived: e})}
clubFilter={filter.club} clubFilter={filter.club}
setClubFilter={e => setFilter({...filter, club: e})} setClubFilter={e => setFilter({...filter, club: e})}
source={source} source={source}
@@ -523,6 +526,8 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
function FiltreBar({ function FiltreBar({
showLicenceState, showLicenceState,
setShowLicenceState, setShowLicenceState,
showArchived,
setShowArchived,
clubFilter, clubFilter,
setClubFilter, setClubFilter,
source, source,
@@ -538,6 +543,9 @@ function FiltreBar({
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/> <Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
</div> </div>
<div className="mb-3">
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label="Afficher les combattants inactifs"/>
</div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}> <select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option> <option value="">--- toute les catégories ---</option>

View File

@@ -4,13 +4,14 @@ import {AxiosError} from "../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import {apiAxios, errFormater} from "../utils/Tools.js"; import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx"; import {SearchBar} from "../components/SearchBar.jsx";
import {ConfirmDialog} from "../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons"; import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
import "./PayAndValidateList.css"; import "./PayAndValidateList.css";
import * as Tools from "../utils/Tools.js";
export function PayAndValidateList({source}) { export function PayAndValidateList({source}) {
@@ -22,6 +23,7 @@ export function PayAndValidateList({source}) {
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]); const [licenceData, setLicenceData] = useState([]);
const [clubFilter, setClubFilter] = useState(""); const [clubFilter, setClubFilter] = useState("");
const [catFilter, setCatFilter] = useState("");
const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2) const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2)
const [lastSearch, setLastSearch] = useState(""); const [lastSearch, setLastSearch] = useState("");
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2); const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
@@ -34,15 +36,15 @@ export function PayAndValidateList({source}) {
data, data,
error, error,
refresh refresh
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1) } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1)
useEffect(() => { useEffect(() => {
sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers)); sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers));
}, [selectedMembers]); }, [selectedMembers]);
useEffect(() => { useEffect(() => {
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`); refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]); }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]);
useEffect(() => { useEffect(() => {
if (!data) if (!data)
@@ -62,7 +64,7 @@ export function PayAndValidateList({source}) {
setMemberData(data2); setMemberData(data2);
}, [data, licenceData]); }, [data, licenceData]);
useEffect(() => { const fetchLicenceData = () => {
toast.promise( toast.promise(
apiAxios.get(`/licence/current/${source}`), apiAxios.get(`/licence/current/${source}`),
{ {
@@ -77,6 +79,10 @@ export function PayAndValidateList({source}) {
.then(data => { .then(data => {
setLicenceData(data.data); setLicenceData(data.data);
}); });
};
useEffect(() => {
fetchLicenceData();
}, []); }, []);
const search = (search) => { const search = (search) => {
@@ -104,7 +110,31 @@ export function PayAndValidateList({source}) {
} }
).then(() => { ).then(() => {
setSelectedMembers([]); setSelectedMembers([]);
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`); refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
});
}
const handlePayment = () => {
if (selectedMembers.length === 0) {
toast.error("Aucun membre sélectionné");
return;
}
toast.promise(
apiAxios.post(`/licence/validate-pay`, selectedMembers),
{
pending: "Validation des licences en cours...",
success: "Licences validées avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la validation des licences")
}
}
}
).then(() => {
setSelectedMembers([]);
fetchLicenceData();
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
}); });
} }
@@ -153,13 +183,20 @@ export function PayAndValidateList({source}) {
<div className="card-body"> <div className="card-body">
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source} <FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter} stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
setPaymentFilter={setPaymentFilter}/> setPaymentFilter={setPaymentFilter} setCatFilter={setCatFilter} catFilter={catFilter}/>
</div> </div>
</div> </div>
<div className="mb-4"> <div className="mb-4">
{source === "admin" && <> {source === "admin" && <>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation">Valider <button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-pay">Valider le payement
des {selectedMembers.length} licences sélectionnée
</button>
<ConfirmDialog title="Payment des licences"
message={"Êtes-vous sûr de vouloir marquer comme payées les " + selectedMembers.length + " licences ?"}
onConfirm={handlePayment} id="confirm-pay"/>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation" style={{marginTop: "0.5em"}}>Valider
les {selectedMembers.length} licences sélectionnée les {selectedMembers.length} licences sélectionnée
</button> </button>
<ConfirmDialog title="Validation des licences" <ConfirmDialog title="Validation des licences"
@@ -385,7 +422,7 @@ function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
let allClub = [] let allClub = []
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) { function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter, setCatFilter, catFilter}) {
useEffect(() => { useEffect(() => {
if (!data) if (!data)
return; return;
@@ -395,6 +432,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
return <div> return <div>
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>} {source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
<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>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}> <select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
<option value={1}>Avec demande ou licence validée</option> <option value={1}>Avec demande ou licence validée</option>

View File

@@ -67,10 +67,10 @@ export function InformationForm({data}) {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
} }
}).then(_ => { }).then(_ => {
toast.success('Profile mis à jours avec succès 🎉'); toast.success('Profil mis à jours avec succès 🎉');
}).catch(e => { }).catch(e => {
console.log(e.response) console.log(e.response)
toast.error(errFormater(e,'Échec de la mise à jours du profile 😕')); toast.error(errFormater(e,'Échec de la mise à jours du profil 😕'));
}).finally(() => { }).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)

View File

@@ -155,8 +155,8 @@ function ModalContent2({clubData, data}) {
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<p>Veuillez sélectionner 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain, <p>Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain,
vous pourrez toujours remplacer un des membres sélectionné à la prochaine étape)</p> vous pourrez les renseigner à la prochaine étape)</p>
{data && data.map((d, index) => { {data && data.map((d, index) => {
return <div key={index} className="input-group mb-1"> return <div key={index} className="input-group mb-1">
<div className="input-group-text"> <div className="input-group-text">

View File

@@ -147,15 +147,13 @@ function SearchMember({sendRegister}) {
const handleAdd = (name) => { const handleAdd = (name) => {
const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name); const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name);
console.log("Quick add licence:", member)
if (!member) { if (!member) {
toast.error("Combattant non trouvé"); toast.error("Combattant non trouvé");
return; return;
} }
sendRegister({ sendRegister({
licence: member.licence, fname: member.fname, lname: member.lname, weight: "", overCategory: 0, lockEdit: false, id: null licence: member.licence.trim(), fname: member.fname.trim(), lname: member.lname.trim(), weight: "", overCategory: 0, lockEdit: false, id: null
}) })
} }

View File

@@ -67,6 +67,9 @@ function CategoryHeader({cat, setCatId}) {
useEffect(() => { useEffect(() => {
if (cats && cats.length > 0 && !cat) { if (cats && cats.length > 0 && !cat) {
setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id); setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id);
} else if (cats && cats.length === 0) {
setModal({});
bthRef.current.click();
} }
}, [cats]); }, [cats]);
@@ -77,7 +80,6 @@ function CategoryHeader({cat, setCatId}) {
} else { // New category } else { // New category
setModal({}); setModal({});
bthRef.current.click(); bthRef.current.click();
console.log(cat);
e.target.value = cat?.id; e.target.value = cat?.id;
} }
} }

View File

@@ -116,7 +116,7 @@ export function CategoryContent({cat, catId, setCat}) {
const activeMatches = matches2.filter(m => m.poule !== '-') const activeMatches = matches2.filter(m => m.poule !== '-')
const combsIDs = activeMatches.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i) const combsIDs = matches2.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i)
.map(d => { .map(d => {
let poule = activeMatches.find(m => (m.c1 === d || m.c2 === d) && m.categorie_ord !== -42)?.poule let poule = activeMatches.find(m => (m.c1 === d || m.c2 === d) && m.categorie_ord !== -42)?.poule
if (!poule) if (!poule)

View File

@@ -111,11 +111,11 @@ function Home2({perm}) {
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4> <h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4>
<div className="col"> <div className="col">
{perm === "ADMIN" && <> {perm === "ADMIN" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button> <button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button>
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button> <button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button>
</>} </>}
{perm === "TABLE" && <> {perm === "TABLE" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button> <button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button>
</>} </>}
</div> </div>
</div> </div>