41 Commits

Author SHA1 Message Date
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
16 changed files with 272 additions and 104 deletions

View File

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

View File

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

View File

@@ -270,13 +270,99 @@ 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", String.format(
"FFSAF - Creation de votre compte sur l'intranet", """
String.format( <!DOCTYPE html>
<html data-lt-installed="true">
<head>
<meta charset="UTF-8">
<title>Création de votre compte intranet FFSAF</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>
<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>
<p>Cordialement,<br>L&rsquo;&eacute;quipe de la FFSAF</p>
</div>
</body>
</html>
""",
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername())
)
.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 +377,8 @@ public class KeycloakService {
L'équipe de la FFSAF L'équipe de la FFSAF
""", """,
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub() membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername()) .getName() + ") " : "", user.getUsername()))
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr") .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
) : Uni.createFrom().nullItem()) ) : Uni.createFrom().nullItem())
.call(user -> membreService.setUserId(membreModel.getId(), user.getId())) .call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub())); .call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -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)
@@ -114,4 +114,4 @@ export function InformationForm({data}) {
</div> </div>
</div> </div>
</form>; </form>;
} }

View File

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

View File

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