56 Commits

Author SHA1 Message Date
b7a1e58436 fix: cma comb dupli on poule list 2025-12-27 17:34:58 +01:00
a73a3562c3 feat: add scrolle to match list in cmt 2025-12-27 17:29:28 +01:00
0dace76fc3 feat: cat remove 2025-12-27 17:06:27 +01:00
f76ca43ed6 feat: add import fail log detail 2025-12-27 16:55:23 +01:00
c6b24a1b20 feat: add fname formatter 2025-12-27 16:43:14 +01:00
c47c04c29e fix: cat update/add refresh 2025-12-27 15:00:50 +01:00
e06df24002 ty-fix: ws timeout 2025-12-27 14:20:10 +01:00
352c34064f fix: score save update 2025-12-27 14:18:38 +01:00
2ccd2a9178 fix: disable match filter on 1 zone cat + rename lice to zone 2025-12-27 13:02:45 +01:00
847674d62c feat: add club_tresorier to club admin
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m46s
2025-12-26 22:18:03 +01:00
68b4f8e476 fix: delect club without user
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m40s
2025-12-26 14:35:19 +01: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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
35 changed files with 574 additions and 246 deletions

View File

@@ -355,19 +355,25 @@ 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>
Bonjour, <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,
Votre demande d'affiliation pour le club %s a été acceptée. Votre demande d'affiliation pour le club %s a été acceptée.
Le numéro d'affiliation de votre club est le %d. Le numéro d'affiliation de votre club est le %d.
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,21 +464,27 @@ 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>
Bonjour, <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,
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):
%s %s
Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr. Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
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)))
@@ -282,7 +287,8 @@ public class ClubService {
combModel.setClub(null); combModel.setClub(null);
combModel.setRole(RoleAsso.MEMBRE); combModel.setRole(RoleAsso.MEMBRE);
}).toList()) }).toList())
.call(list -> (list.isEmpty()) ? Uni.createFrom().voidItem() : .call(list -> list.stream().noneMatch(m -> m.getUserId() != null) ?
Uni.createFrom().voidItem() :
Uni.join().all(list.stream().filter(m -> m.getUserId() != null) Uni.join().all(list.stream().filter(m -> m.getUserId() != null)
.map(m -> keycloakService.clearUser(m.getUserId())).toList()) .map(m -> keycloakService.clearUser(m.getUserId())).toList())
.andCollectFailures()) .andCollectFailures())
@@ -323,9 +329,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,24 +156,32 @@ 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/>
Bonjour, 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,
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,
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci. vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
Ancienne adresse email : %s Ancienne adresse email : %s
Nouvelle adresse email : %s Nouvelle adresse email : %s
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr. Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
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,16 @@ 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é par " + model.getFname() + " " + model.getFname());
} }
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é par " + model.getFname() + " " + model.getFname());
} }
} }
@@ -305,7 +290,8 @@ public class MembreService {
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( throw new DBadRequestException(
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide."); "Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (tentative de changement non-autotiser de nom sur la licence "
+ model.getLicence() + " pour " + model.getFname() + " " + model.getFname() + ")");
} }
ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model); ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model);
@@ -313,8 +299,7 @@ public class MembreService {
dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model); dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model);
model.setLname(dataIn.getNom().toUpperCase()); model.setLname(dataIn.getNom().toUpperCase());
model.setFname(dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1)); model.setFname(Utils.formatPrenom(dataIn.getPrenom()));
if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) { if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) {
ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model); ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model);
@@ -380,7 +365,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 +387,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()))
@@ -434,7 +419,7 @@ public class MembreService {
private Uni<String> update(Uni<MembreModel> uni, FullMemberForm membre, boolean admin) { private Uni<String> update(Uni<MembreModel> uni, FullMemberForm membre, boolean admin) {
return uni.chain(target -> { return uni.chain(target -> {
ls.logChange("Prénom", target.getFname(), membre.getFname(), target); ls.logChange("Prénom", target.getFname(), membre.getFname(), target);
target.setFname(membre.getFname()); target.setFname(Utils.formatPrenom(membre.getFname()));
ls.logChange("Nom", target.getLname(), membre.getLname(), target); ls.logChange("Nom", target.getLname(), membre.getLname(), target);
target.setLname(membre.getLname().toUpperCase()); target.setLname(membre.getLname().toUpperCase());
ls.logChange("Pays", target.getCountry(), membre.getCountry(), target); ls.logChange("Pays", target.getCountry(), membre.getCountry(), target);
@@ -492,7 +477,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 +493,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(
@@ -577,8 +562,8 @@ public class MembreService {
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) { private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
MembreModel model = new MembreModel(); MembreModel model = new MembreModel();
model.setFname(input.getFname()); model.setFname(Utils.formatPrenom(input.getFname()));
model.setLname(input.getLname()); model.setLname(input.getLname().toUpperCase());
model.setEmail(input.getEmail()); model.setEmail(input.getEmail());
model.setLicence(null); model.setLicence(null);
model.setGenre(input.getGenre()); model.setGenre(input.getGenre());

View File

@@ -49,7 +49,7 @@ public class AffiliationEndpoints {
@GET @GET
@Path("{id}") @Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les affiliations pour un club", description = "Cette méthode renvoie les affiliations pour un club donné. Seuls les administrateurs de la fédération et les présidents, secrétaires et responsables intranet du club peuvent accéder à cette méthode.") @Operation(summary = "Renvoie les affiliations pour un club", description = "Cette méthode renvoie les affiliations pour un club donné. Seuls les administrateurs de la fédération et les présidents, secrétaires et responsables intranet du club peuvent accéder à cette méthode.")
@APIResponses(value = { @APIResponses(value = {

View File

@@ -73,7 +73,7 @@ public class AffiliationRequestEndpoints {
@GET @GET
@Path("/{id}") @Path("/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie une demande d'affiliation", description = "Cette méthode renvoie une demande d'affiliation " + @Operation(summary = "Renvoie une demande d'affiliation", description = "Cette méthode renvoie une demande d'affiliation " +
"pour l'identifiant spécifié.") "pour l'identifiant spécifié.")
@@ -92,7 +92,7 @@ public class AffiliationRequestEndpoints {
@DELETE @DELETE
@Path("/{id}") @Path("/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " + @Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " +
"d'affiliation pour l'identifiant spécifié.") "d'affiliation pour l'identifiant spécifié.")
@@ -128,7 +128,7 @@ public class AffiliationRequestEndpoints {
@PUT @PUT
@Path("/edit") @Path("/edit")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Modifie une demande d'affiliation", description = "Cette méthode modifie une demande " + @Operation(summary = "Modifie une demande d'affiliation", description = "Cette méthode modifie une demande " +

View File

@@ -109,7 +109,7 @@ public class ClubEndpoints {
@GET @GET
@Path("{id}") @Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les détails d'un club en fonction de son identifiant", description = "Renvoie les " + @Operation(summary = "Renvoie les détails d'un club en fonction de son identifiant", description = "Renvoie les " +
"détails d'un club en fonction de son identifiant, y compris les informations sur les membres et les affiliations") "détails d'un club en fonction de son identifiant, y compris les informations sur les membres et les affiliations")
@@ -230,7 +230,7 @@ public class ClubEndpoints {
@GET @GET
@Path("/me") @Path("/me")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les informations du club de l'utilisateur connecté", description = "Renvoie les " + @Operation(summary = "Renvoie les informations du club de l'utilisateur connecté", description = "Renvoie les " +
"informations du club de l'utilisateur connecté") "informations du club de l'utilisateur connecté")
@@ -247,7 +247,7 @@ public class ClubEndpoints {
@PUT @PUT
@Path("/me") @Path("/me")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Met à jour les informations du club de l'utilisateur connecté", description = "Met à jour les" + @Operation(summary = "Met à jour les informations du club de l'utilisateur connecté", description = "Met à jour les" +
@@ -264,7 +264,7 @@ public class ClubEndpoints {
@GET @GET
@Path("/me/affiliation") @Path("/me/affiliation")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie l'attestation d'affiliation du club de l'utilisateur connecté", description = @Operation(summary = "Renvoie l'attestation d'affiliation du club de l'utilisateur connecté", description =
"Renvoie l'attestation d'affiliation du club de l'utilisateur connecté") "Renvoie l'attestation d'affiliation du club de l'utilisateur connecté")
@APIResponses(value = { @APIResponses(value = {
@@ -296,7 +296,7 @@ public class ClubEndpoints {
@GET @GET
@Path("/renew/{id}") @Path("/renew/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true) @Operation(hidden = true)
public Uni<RenewAffData> getRenew(@PathParam("id") long id, @QueryParam("m1") long m1_id, public Uni<RenewAffData> getRenew(@PathParam("id") long id, @QueryParam("m1") long m1_id,
@@ -307,7 +307,7 @@ public class ClubEndpoints {
@GET @GET
@Path("/desk/{id}") @Path("/desk/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie la liste des membres du bureau du club", description = "Renvoie la liste des membres " + @Operation(summary = "Renvoie la liste des membres du bureau du club", description = "Renvoie la liste des membres " +
"du bureau du club spécifié") "du bureau du club spécifié")
@@ -345,7 +345,7 @@ public class ClubEndpoints {
@GET @GET
@Path("{id}/status") @Path("{id}/status")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie le statut du club", description = "Renvoie le statut du club spécifié") @Operation(summary = "Renvoie le statut du club", description = "Renvoie le statut du club spécifié")
@APIResponses(value = { @APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le statut du club"), @APIResponse(responseCode = "200", description = "Le statut du club"),

View File

@@ -37,7 +37,7 @@ public class CompteEndpoints {
@GET @GET
@Path("{id}") @Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie les informations d'un compte utilisateur", description = "Renvoie les informations d'un" + @Operation(summary = "Renvoie les informations d'un compte utilisateur", description = "Renvoie les informations d'un" +
" compte utilisateur en fonction de son identifiant long (UUID)") " compte utilisateur en fonction de son identifiant long (UUID)")
@APIResponses(value = { @APIResponses(value = {

View File

@@ -36,7 +36,7 @@ public class LicenceEndpoints {
@GET @GET
@Path("{id}") @Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les licences d'un membre", description = "Renvoie les licences d'un membre en fonction " + @Operation(summary = "Renvoie les licences d'un membre", description = "Renvoie les licences d'un membre en fonction " +
"de son identifiant") "de son identifiant")
@@ -68,7 +68,7 @@ public class LicenceEndpoints {
@GET @GET
@Path("current/club") @Path("current/club")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les licences de la saison en cours (pour les clubs)", description = "Renvoie les " + @Operation(summary = "Renvoie les licences de la saison en cours (pour les clubs)", description = "Renvoie les " +
"licences de la saison en cours (pour les clubs)") "licences de la saison en cours (pour les clubs)")
@@ -83,7 +83,7 @@ public class LicenceEndpoints {
@POST @POST
@Path("pay") @Path("pay")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Paiement des licence", description = "Retourne le lien de paiement pour les licence des membre fournie") @Operation(summary = "Paiement des licence", description = "Retourne le lien de paiement pour les licence des membre fournie")
@@ -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")
@@ -146,7 +161,7 @@ public class LicenceEndpoints {
@POST @POST
@Path("club/{id}") @Path("club/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Demande une nouvelle licence", description = "Demande une nouvelle licence en fonction de" + @Operation(summary = "Demande une nouvelle licence", description = "Demande une nouvelle licence en fonction de" +
@@ -163,7 +178,7 @@ public class LicenceEndpoints {
@DELETE @DELETE
@Path("club/{id}") @Path("club/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Supprime une demande de licence", description = "Supprime une demande de licence en fonction " + @Operation(summary = "Supprime une demande de licence", description = "Supprime une demande de licence en fonction " +
"de son identifiant (pour les clubs)") "de son identifiant (pour les clubs)")

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

@@ -24,7 +24,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.util.List; import java.util.List;
@Tag(name = "Membre club", description = "Gestion des membres (pour les clubs)") @Tag(name = "Membre club", description = "Gestion des membres (pour les clubs)")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Path("api/member") @Path("api/member")
public class MembreClubEndpoints { public class MembreClubEndpoints {
@@ -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

@@ -51,7 +51,7 @@ public class MembreEndpoints {
@GET @GET
@Path("{id}") @Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les détails d'un membre en fonction de son identifiant", description = "Renvoie les " + @Operation(summary = "Renvoie les détails d'un membre en fonction de son identifiant", description = "Renvoie les " +
"détails d'un membre en fonction de son identifiant") "détails d'un membre en fonction de son identifiant")
@@ -68,7 +68,7 @@ public class MembreEndpoints {
@GET @GET
@Path("/find/licence") @Path("/find/licence")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie les détails d'un membre en fonction de son numéro de licence", description = "Renvoie " + @Operation(summary = "Renvoie les détails d'un membre en fonction de son numéro de licence", description = "Renvoie " +
"les détails d'un membre en fonction de son numéro de licence") "les détails d'un membre en fonction de son numéro de licence")
@APIResponses(value = { @APIResponses(value = {
@@ -130,7 +130,7 @@ public class MembreEndpoints {
@GET @GET
@Path("{id}/photo") @Path("{id}/photo")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie la photo d'un membre", description = "Renvoie la photo d'un membre en fonction de son identifiant") @Operation(summary = "Renvoie la photo d'un membre", description = "Renvoie la photo d'un membre en fonction de son identifiant")
@APIResponses(value = { @APIResponses(value = {
@APIResponse(responseCode = "200", description = "La photo du membre"), @APIResponse(responseCode = "200", description = "La photo du membre"),
@@ -146,7 +146,7 @@ public class MembreEndpoints {
@GET @GET
@Path("{id}/licence") @Path("{id}/licence")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie le pdf de la licence d'un membre", description = "Renvoie le pdf de la licence d'un membre en fonction de son identifiant") @Operation(summary = "Renvoie le pdf de la licence d'un membre", description = "Renvoie le pdf de la licence d'un membre en fonction de son identifiant")
@APIResponses(value = { @APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le pdf de la licence"), @APIResponse(responseCode = "200", description = "Le pdf de la licence"),

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());
} }
@@ -262,4 +335,34 @@ public class Utils {
return (int) ((calendar.getTimeInMillis() - now.getTimeInMillis()) / (1000 * 60 * 60 * 24)); return (int) ((calendar.getTimeInMillis() - now.getTimeInMillis()) / (1000 * 60 * 60 * 24));
} }
public static String formatPrenom(String input) {
if (input == null || input.isEmpty()) {
return input;
}
StringBuilder result = new StringBuilder();
String[] mots = input.split(" ");
for (String mot : mots) {
if (!mot.isEmpty()) {
String[] parties = mot.split("[-']");
StringBuilder motFormate = new StringBuilder();
for (int i = 0; i < parties.length; i++) {
if (!parties[i].isEmpty()) {
String premiereLettre = parties[i].substring(0, 1).toUpperCase();
String reste = parties[i].substring(1).toLowerCase();
motFormate.append(premiereLettre).append(reste);
}
if (i < parties.length - 1) {
motFormate.append(mot.charAt(mot.indexOf(parties[i]) + parties[i].length()));
}
}
result.append(motFormate).append(" ");
}
}
return result.toString().trim();
}
} }

View File

@@ -21,6 +21,7 @@ import org.jboss.logging.Logger;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*; import java.util.*;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@@ -166,6 +167,7 @@ public class CompetitionWS {
return Uni.createFrom().item(makeError(message, "Permission denied")).toMulti(); return Uni.createFrom().item(makeError(message, "Permission denied")).toMulti();
return ((Uni<?>) method.invoke(entry.getValue(), connection, return ((Uni<?>) method.invoke(entry.getValue(), connection,
MAPPER.treeToValue(message.data(), method.getParameterTypes()[1]))) MAPPER.treeToValue(message.data(), method.getParameterTypes()[1])))
.ifNoItem().after(Duration.ofSeconds(5)).fail()
.map(o -> makeReply(message, o)) .map(o -> makeReply(message, o))
.onFailure() .onFailure()
.recoverWithItem(t -> { .recoverWithItem(t -> {

View File

@@ -118,7 +118,8 @@ public class RCategorie {
uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId())) uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId()))
.chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat)); .chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat));
} }
return uni; Uni<Long> finalUni = uni;
return Panache.withTransaction(() -> finalUni);
}) })
.call(cat -> SSCategorie.sendCategory(connection, cat)) .call(cat -> SSCategorie.sendCategory(connection, cat))
.replaceWithVoid(); .replaceWithVoid();
@@ -205,6 +206,16 @@ public class RCategorie {
.replaceWithVoid(); .replaceWithVoid();
} }
@WSReceiver(code = "deleteCategory", permission = PermLevel.ADMIN)
public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) {
return getById(id, connection)
.call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId())
.call(__ -> matchRepository.delete("category = ?1", cat))))
.chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat)))
.call(__ -> SSCategorie.sendDelCategory(connection, id))
.replaceWithVoid();
}
@RegisterForReflection @RegisterForReflection
public record JustCategorie(long id, String name, int type, String liceName) { public record JustCategorie(long id, String name, int type, String liceName) {
public static JustCategorie from(CategoryModel m) { public static JustCategorie from(CategoryModel m) {

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))

View File

@@ -30,4 +30,8 @@ public class SSCategorie {
public static Uni<Void> sendTreeCategory(WebSocketConnection connection, List<TreeEntity> treeEntities) { public static Uni<Void> sendTreeCategory(WebSocketConnection connection, List<TreeEntity> treeEntities) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", treeEntities); return CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", treeEntities);
} }
public static Uni<?> sendDelCategory(WebSocketConnection connection, Long id) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendDelCategory", id);
}
} }

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

@@ -2,6 +2,7 @@ import './Nav.css'
import {NavLink} from "react-router-dom"; import {NavLink} from "react-router-dom";
import {useAuth} from "../hooks/useAuth.jsx"; import {useAuth} from "../hooks/useAuth.jsx";
import {login, logout} from "../utils/auth.js"; import {login, logout} from "../utils/auth.js";
import {isClubAdmin} from "../utils/Tools.js";
export function Nav() { export function Nav() {
@@ -55,7 +56,8 @@ function CompMenu() {
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions Manager</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions
Manager</NavLink></li>
</ul> </ul>
</li> </li>
} }
@@ -63,8 +65,7 @@ function CompMenu() {
function ClubMenu() { function ClubMenu() {
const {is_authenticated, userinfo} = useAuth() const {is_authenticated, userinfo} = useAuth()
if (!is_authenticated || !(userinfo?.roles?.includes("club_president") if (!is_authenticated || !(isClubAdmin(userinfo)))
|| userinfo?.roles?.includes("club_secretaire") || userinfo?.roles?.includes("club_respo_intra")))
return <></> return <></>
return <li className="nav-item dropdown"> return <li className="nav-item dropdown">

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

@@ -15,16 +15,16 @@ export function CMAdmin() {
const categoryListener = ({data}) => { const categoryListener = ({data}) => {
if (!cat || data.id !== cat.id) if (!cat || data.id !== cat.id)
return return
setCat({ setCat(cat_ => ({
...cat, ...cat_,
name: data.name, name: data.name,
liceName: data.liceName, liceName: data.liceName,
type: data.type type: data.type
}) }))
} }
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener}) return () => dispatch({type: 'removeListener', payload: categoryListener})
}, []); }, [cat]);
return <> return <>
<div className="card"> <div className="card">
@@ -60,15 +60,34 @@ function CategoryHeader({cat, setCatId}) {
data data
]) ])
} }
const sendAddCategory = ({data}) => {
setCats([...cats, data])
}
const sendDelCategory = ({data}) => {
setCatId(catId => {
if (catId === data) return null;
return catId;
})
setCats([...cats.filter(c => c.id !== data)])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener}) dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
return () => {
dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'removeListener', payload: sendAddCategory})
dispatch({type: 'removeListener', payload: sendDelCategory})
}
}, [cats]); }, [cats]);
useEffect(() => { useEffect(() => {
if (cats && cats.length > 0 && !cat) { if (cats && cats.length > 0 && !cat || (cats && !cats.find(c => c.id === cat.id))) {
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, cat]);
const handleCatChange = (e) => { const handleCatChange = (e) => {
const selectedCatId = e.target.value; const selectedCatId = e.target.value;
@@ -77,7 +96,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;
} }
} }
@@ -86,7 +104,7 @@ function CategoryHeader({cat, setCatId}) {
<div className="col-auto"> <div className="col-auto">
<div className="input-group"> <div className="input-group">
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5> <h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5>
<select className="form-select" onChange={handleCatChange}> <select className="form-select" onChange={handleCatChange} value={cat?.id || ""}>
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => ( {cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))} <option key={c.id} value={c.id}>{c.name}</option>))}
{cats && <option value={-1}>Nouvelle...</option>} {cats && <option value={-1}>Nouvelle...</option>}
@@ -96,7 +114,7 @@ function CategoryHeader({cat, setCatId}) {
<div className="col" style={{margin: "auto 0", textAlign: "center"}}> <div className="col" style={{margin: "auto 0", textAlign: "center"}}>
{cat && {cat &&
<div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} | <div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} |
Lice: {cat.liceName}</div>} Zone: {cat.liceName}</div>}
</div> </div>
<div className="col-auto"> <div className="col-auto">
<button className="btn btn-primary float-end" onClick={() => { <button className="btn btn-primary float-end" onClick={() => {
@@ -126,7 +144,7 @@ function CategoryHeader({cat, setCatId}) {
function ModalContent({state, setCatId, setConfirm, confirmRef}) { function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const [name, setName] = useState("") const [name, setName] = useState("")
const [lice, setLice] = useState("1") const [lice, setLice] = useState("A")
const [poule, setPoule] = useState(true) const [poule, setPoule] = useState(true)
const [tournoi, setTournoi] = useState(false) const [tournoi, setTournoi] = useState(false)
const [size, setSize] = useState(4) const [size, setSize] = useState(4)
@@ -136,7 +154,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
useEffect(() => { useEffect(() => {
setName(state.name || ""); setName(state.name || "");
setLice(state.liceName || "1"); setLice(state.liceName || "A");
setPoule(((state.type || 1) & 1) !== 0); setPoule(((state.type || 1) & 1) !== 0);
setTournoi((state.type & 2) !== 0); setTournoi((state.type & 2) !== 0);
@@ -162,7 +180,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const regex = /^([^;]+;)*[^;]+$/; const regex = /^([^;]+;)*[^;]+$/;
if (regex.test(lice.trim()) === false) { if (regex.test(lice.trim()) === false) {
toast.error("Le format du nom des lices est invalide. Veuillez séparer les noms par des ';'."); toast.error("Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.");
return; return;
} }
@@ -268,8 +286,6 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
error: 'Erreur lors de la création de la catégorie' error: 'Erreur lors de la création de la catégorie'
} }
).then(id => { ).then(id => {
setCatId(id);
if (tournoi) { if (tournoi) {
const trees = build_tree(size, loserMatch) const trees = build_tree(size, loserMatch)
console.log("Creating trees for new category:", trees); console.log("Creating trees for new category:", trees);
@@ -280,7 +296,9 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
success: 'Arbres créés !', success: 'Arbres créés !',
error: 'Erreur lors de la création des arbres' error: 'Erreur lors de la création des arbres'
} }
) ).finally(() => setCatId(id))
} else {
setCatId(id);
} }
}) })
} }
@@ -297,7 +315,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
return <form onSubmit={handleSubmit}> return <form onSubmit={handleSubmit}>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">Ajouter une catégorie</h1> <h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? "Ajouter" : "Modifier"} une catégorie</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" <button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
@@ -309,8 +327,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
</div> </div>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="liceInput1" className="form-label">Nom des lices <small>(séparée par des ';')</small></label> <label htmlFor="liceInput1" className="form-label">Nom des zones de combat <small>(séparée par des ';')</small></label>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="lice" value={lice} <input type="text" className="form-control" id="liceInput1" placeholder="A;B" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/> onChange={e => setLice(e.target.value)}/>
</div> </div>
@@ -371,6 +389,22 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
{state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
setConfirm({
title: "Suppression de la catégorie",
message: `Voulez-vous vraiment supprimer la catégorie ${state.name}. Cela va supprimer tous les matchs associés !`,
confirm: () => {
toast.promise(sendRequest('deleteCategory', state.id),
{
pending: 'Suppression de la catégorie...',
success: 'Catégorie supprimée !',
error: 'Erreur lors de la suppression de la catégorie'
}
).then(() => setCatId(null));
}
})
confirmRef.current.click();
}}>Supprimer</button>}
</div> </div>
</form> </form>
} }

View File

@@ -27,8 +27,22 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
const categoryListener = ({data}) => { const categoryListener = ({data}) => {
setCats([...cats.filter(c => c.id !== data.id), data]) setCats([...cats.filter(c => c.id !== data.id), data])
} }
const sendAddCategory = ({data}) => {
setCats([...cats, data])
}
const sendDelCategory = ({data}) => {
if (catId === data)
setCatId(-1);
setCats([...cats.filter(c => c.id !== data)])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener}) dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
return () => {
dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'removeListener', payload: sendAddCategory})
dispatch({type: 'removeListener', payload: sendDelCategory})
}
}, [cats]); }, [cats]);
const cat = cats?.find(c => c.id === catId); const cat = cats?.find(c => c.id === catId);
@@ -131,10 +145,15 @@ function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1); const [type, setType] = useState(1);
useEffect(() => { useEffect(() => {
if (!cat)
return;
if ((cat.type & type) === 0) if ((cat.type & type) === 0)
setType(cat.type); setType(cat.type);
}, [cat]); }, [cat]);
if (!cat)
return <></>;
return <div style={{marginTop: "1em"}}> return <div style={{marginTop: "1em"}}>
{cat && cat.type === 3 && <> {cat && cat.type === 3 && <>
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
@@ -173,6 +192,10 @@ function MatchList({matches, cat, menuActions}) {
.map(m => ({...m, win: win(m.scores)})) .map(m => ({...m, win: win(m.scores)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => {
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
}
const match = matches.find(m => m.id === activeMatch) const match = matches.find(m => m.id === activeMatch)
useEffect(() => { useEffect(() => {
if (!match) { if (!match) {
@@ -183,7 +206,7 @@ function MatchList({matches, cat, menuActions}) {
payload: { payload: {
c1: match.c1, c1: match.c1,
c2: match.c2, c2: match.c2,
next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({ next: marches2.filter((m, index) => !m.end && isActiveMatch(index) && m.id !== activeMatch).map(m => ({
c1: m.c1, c1: m.c1,
c2: m.c2 c2: m.c2
})) }))
@@ -198,7 +221,7 @@ function MatchList({matches, cat, menuActions}) {
useEffect(() => { useEffect(() => {
if (match && match.poule !== lice) if (match && match.poule !== lice)
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id) setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
}, [lice]); }, [lice]);
useEffect(() => { useEffect(() => {
@@ -207,12 +230,12 @@ function MatchList({matches, cat, menuActions}) {
if (marches2.some(m => m.id === activeMatch)) if (marches2.some(m => m.id === activeMatch))
return; return;
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id); setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
}, [matches]) }, [matches])
return <> return <>
{liceName.length > 1 && {liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}> <div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
<label className="input-group-text" htmlFor="selectLice">Lice</label> <label className="input-group-text" htmlFor="selectLice">Zone de combat</label>
<select className="form-select" id="selectLice" value={lice} onChange={e => { <select className="form-select" id="selectLice" value={lice} onChange={e => {
setLice(e.target.value); setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value); localStorage.setItem("cm_lice", e.target.value);
@@ -224,11 +247,11 @@ function MatchList({matches, cat, menuActions}) {
</div> </div>
} }
<div className="table-responsive-xxl"> <div className="table-responsive-xxl overflow-y-auto" style={{maxHeight: "50vh"}}>
<table className="table table-striped table-hover"> <table className="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
@@ -240,7 +263,7 @@ function MatchList({matches, cat, menuActions}) {
<tbody className="table-group-divider"> <tbody className="table-group-divider">
{marches2.map((m, index) => ( {marches2.map((m, index) => (
<tr key={m.id} <tr key={m.id}
className={m.id === activeMatch ? "table-info" : (liceName[(index - firstIndex) % liceName.length] === lice ? "" : "table-warning")} className={m.id === activeMatch ? "table-info" : (isActiveMatch(index) ? "" : "table-warning")}
onClick={() => setActiveMatch(m.id)}> onClick={() => setActiveMatch(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}> <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td> {liceName[(index - firstIndex) % liceName.length]}</td>
@@ -259,7 +282,7 @@ function MatchList({matches, cat, menuActions}) {
</table> </table>
</div> </div>
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match} menuActions={menuActions}/></LoadingProvider>} {activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</> </>
} }
@@ -325,27 +348,29 @@ function BuildTree({treeData, matches, menuActions}) {
return <div> return <div>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}> <div className="overflow-y-auto" style={{maxHeight: "50vh"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid} <div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/> <DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
</div>
</div> </div>
{currentMatch?.matchSelect && {currentMatch?.matchSelect &&
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match} menuActions={menuActions}/></LoadingProvider>} <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</div> </div>
} }
function ScorePanel({matchId, match, menuActions}) { function ScorePanel({matchId, matchs, match, menuActions}) {
const onClickVoid = useRef(() => { const onClickVoid = useRef(() => {
}); });
return <div className="row" onClick={onClickVoid.current}> return <div className="row" onClick={onClickVoid.current}>
<ScorePanel_ matchId={matchId} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/> <ScorePanel_ matchId={matchId} matchs={matchs} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
<CardPanel matchId={matchId} match={match}/> <CardPanel matchId={matchId} match={match}/>
</div> </div>
} }
function ScorePanel_({matchId, match, menuActions, onClickVoid_}) { function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const {sendRequest} = useWS() const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
@@ -355,6 +380,11 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
const tableRef = useRef(null) const tableRef = useRef(null)
const scoreRef = useRef([]) const scoreRef = useRef([])
const lastScoreClick = useRef(null) const lastScoreClick = useRef(null)
const scoreInRef = useRef(null)
useEffect(() => {
scoreInRef.current = scoreIn;
}, [scoreIn]);
useEffect(() => { useEffect(() => {
menuActions.current.saveScore = (scoreRed, scoreBlue) => { menuActions.current.saveScore = (scoreRed, scoreBlue) => {
@@ -403,9 +433,12 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
const {matchId, round, comb} = lastScoreClick.current; const {matchId, round, comb} = lastScoreClick.current;
lastScoreClick.current = null; lastScoreClick.current = null;
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn); const scoreIn_ = String(scoreInRef.current).trim() === "" ? -1000 : Number(scoreInRef.current);
const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round);
console.log("Updating score", matchId, round, comb, scoreIn_, score);
const score = match.scores.find(s => s.n_round === round);
let newScore; let newScore;
if (score) { if (score) {
if (comb === 1) if (comb === 1)
@@ -474,6 +507,18 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
setEnd(match.end); setEnd(match.end);
}, [match]); }, [match]);
useEffect(() => {
const handleClickOutside = (event) => {
if (inputRef.current && !inputRef.current.contains(event.target)) {
onClickVoid();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const o = [...tooltipTriggerList] const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))

View File

@@ -49,9 +49,6 @@ export function CMTable() {
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/> <CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
</div> </div>
</div> </div>
<div style={{backgroundColor: "#c70000"}}>
D
</div>
</div> </div>
</div> </div>
<Menu menuActions={menuActions}/> <Menu menuActions={menuActions}/>

View File

@@ -47,10 +47,10 @@ export function CategoryContent({cat, catId, setCat}) {
const treeListener = ({data}) => { const treeListener = ({data}) => {
if (!cat || data.length < 1 || data[0].categorie !== cat.id) if (!cat || data.length < 1 || data[0].categorie !== cat.id)
return return
setCat({ setCat(cat_ => ({
...cat, ...cat_,
trees: data.map(d => from_sendTree(d, true)) trees: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
}) }))
let matches2 = []; let matches2 = [];
let combsToAdd = []; let combsToAdd = [];
@@ -64,10 +64,16 @@ export function CategoryContent({cat, catId, setCat}) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}}); reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}}); combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
if (data.c1 !== null && !groupsRef.current.some(g => g.id === data.c1?.id)) setGroups(prev => {
setGroups(prev => [...prev, {id: data.c1?.id, poule: data.poule}]); if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id))
if (data.c2 !== null && !groupsRef.current.some(g => g.id === data.c2?.id)) return [...prev, {id: data.c1?.id, poule: data.poule}];
setGroups(prev => [...prev, {id: data.c2?.id, poule: data.poule}]); return prev;
})
setGroups(prev => {
if (data.c2 !== null && !prev.some(g => g.id === data.c2?.id))
return [...prev, {id: data.c2?.id, poule: data.poule}];
return prev;
})
} }
} }
@@ -103,7 +109,7 @@ export function CategoryContent({cat, catId, setCat}) {
name: data.name, name: data.name,
liceName: data.liceName, liceName: data.liceName,
type: data.type, type: data.type,
trees: data.trees.map(d => from_sendTree(d, true)) trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
}) })
let matches2 = []; let matches2 = [];
@@ -116,7 +122,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)
@@ -554,7 +560,7 @@ function MatchList({matches, cat, groups, reducer}) {
<tr> <tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Poule</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Poule</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Lice</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Zone</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Rouge</th> <th style={{textAlign: "center"}} scope="col">Rouge</th>
<th style={{textAlign: "center"}} scope="col">Blue</th> <th style={{textAlign: "center"}} scope="col">Blue</th>

View File

@@ -95,7 +95,7 @@ function WSStatus({setPerm}) {
return () => dispatch({type: 'removeListener', payload: welcomeListener}) return () => dispatch({type: 'removeListener', payload: welcomeListener})
}, []) }, [])
return <div className="row"> return <div className="row" style={{marginRight: "inherit"}}>
<h2 className="col">{name}</h2> <h2 className="col">{name}</h2>
<div className="col-auto" style={{margin: "auto 0", padding: 0}}>Serveur: <ColoredCircle <div className="col-auto" style={{margin: "auto 0", padding: 0}}>Serveur: <ColoredCircle
color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/> color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/>
@@ -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>