From 76d7a28678428a31e1d583a77318402d390af3f7 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Thu, 11 Jul 2024 23:02:29 +0200 Subject: [PATCH] wip: club --- pom.xml | 6 + .../data/model/AffiliationRequestModel.java | 30 +-- .../domain/service/AffiliationService.java | 45 +++- .../ffsaf/domain/service/ClubService.java | 67 ++++++ .../ffsaf/net2/data/SimpleClubModel.java | 4 +- .../ffsaf/rest/AffiliationEndpoints.java | 72 ++++++- .../titionfire/ffsaf/rest/AssoEndpoints.java | 25 --- .../titionfire/ffsaf/rest/ClubEndpoints.java | 141 ++++++++++++- .../titionfire/ffsaf/rest/CombEndpoints.java | 38 +--- .../ffsaf/rest/data/SimpleAffiliation.java | 30 +++ .../ffsaf/rest/data/SimpleClub.java | 53 +++++ .../ffsaf/rest/from/AffiliationForm.java | 4 + .../rest/from/AffiliationRequestForm.java | 90 ++++---- .../ffsaf/rest/from/FullClubForm.java | 23 +++ .../fr/titionfire/ffsaf/utils/RoleAsso.java | 10 +- .../ffsaf/utils/StringSimilarity.java | 65 ++++++ .../java/fr/titionfire/ffsaf/utils/Utils.java | 54 ++++- src/main/webapp/index.html | 4 + src/main/webapp/package-lock.json | 30 +++ src/main/webapp/package.json | 2 + .../src/components/MemberCustomFiels.jsx | 16 +- src/main/webapp/src/components/Nav.jsx | 2 +- src/main/webapp/src/components/SearchBar.jsx | 43 ++++ src/main/webapp/src/pages/DemandeAff.jsx | 102 ++++++---- src/main/webapp/src/pages/MemberList.jsx | 36 +--- src/main/webapp/src/pages/admin/AdminRoot.jsx | 20 ++ .../admin/affiliation/AffiliationReqPage.jsx | 16 ++ .../src/pages/admin/club/AffiliationCard.jsx | 187 +++++++++++++++++ .../webapp/src/pages/admin/club/ClubList.jsx | 192 ++++++++++++++++++ .../webapp/src/pages/admin/club/ClubPage.jsx | 128 ++++++++++++ .../src/pages/admin/club/NewClubPage.jsx | 16 ++ .../pages/admin/member/InformationForm.jsx | 11 +- .../src/pages/club/member/InformationForm.jsx | 11 +- 33 files changed, 1346 insertions(+), 227 deletions(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationForm.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java create mode 100644 src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java create mode 100644 src/main/webapp/src/components/SearchBar.jsx create mode 100644 src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx create mode 100644 src/main/webapp/src/pages/admin/club/AffiliationCard.jsx create mode 100644 src/main/webapp/src/pages/admin/club/ClubList.jsx create mode 100644 src/main/webapp/src/pages/admin/club/ClubPage.jsx create mode 100644 src/main/webapp/src/pages/admin/club/NewClubPage.jsx diff --git a/pom.xml b/pom.xml index 6c3aafc..059225b 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,12 @@ io.quarkus quarkus-websockets + + + net.sf.jmimemagic + jmimemagic + 0.1.3 + diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java index fc09800..1c6f38e 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.data.model; +import fr.titionfire.ffsaf.utils.RoleAsso; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import lombok.*; @@ -20,24 +21,27 @@ public class AffiliationRequestModel { Long id; String name; - String siren; + long siren; String RNA; String address; - String president_lname; - String president_fname; - String president_email; - int president_lincence; + String m1_lname; + String m1_fname; + String m1_email; + int m1_lincence; + RoleAsso m1_role; - String tresorier_lname; - String tresorier_fname; - String tresorier_email; - int tresorier_lincence; + String m2_lname; + String m2_fname; + String m2_email; + int m2_lincence; + RoleAsso m2_role; - String secretaire_lname; - String secretaire_fname; - String secretaire_email; - int secretaire_lincence; + String m3_lname; + String m3_fname; + String m3_email; + int m3_lincence; + RoleAsso m3_role; int saison; } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 071b9a8..0a642b0 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -1,8 +1,11 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.repository.AffiliationRequestRepository; import fr.titionfire.ffsaf.data.repository.CombRepository; +import fr.titionfire.ffsaf.rest.data.SimpleAffiliation; +import fr.titionfire.ffsaf.rest.from.AffiliationForm; import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.hibernate.reactive.panache.Panache; @@ -12,6 +15,9 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; +import java.util.List; +import java.util.function.Consumer; + @WithSession @ApplicationScoped public class AffiliationService { @@ -29,32 +35,49 @@ public class AffiliationService { AffiliationRequestModel affModel = form.toModel(); affModel.setSaison(Utils.getSaison()); + // noinspection ResultOfMethodCallIgnored return Uni.createFrom().item(affModel) - .call(model -> ((model.getPresident_lincence() != 0) ? combRepository.find("licence", - model.getPresident_lincence()).count().invoke(count -> { + .call(model -> ((model.getM1_lincence() != 0) ? combRepository.find("licence", + model.getM1_lincence()).count().invoke(count -> { if (count == 0) { - throw new IllegalArgumentException("Licence président inconnue"); + throw new IllegalArgumentException("Licence membre n°1 inconnue"); } }) : Uni.createFrom().nullItem()) ) - .call(model -> ((model.getTresorier_lincence() != 0) ? combRepository.find("licence", - model.getTresorier_lincence()).count().invoke(count -> { + .call(model -> ((model.getM2_lincence() != 0) ? combRepository.find("licence", + model.getM2_lincence()).count().invoke(count -> { if (count == 0) { - throw new IllegalArgumentException("Licence trésorier inconnue"); + throw new IllegalArgumentException("Licence membre n°2 inconnue"); } }) : Uni.createFrom().nullItem()) ) - .call(model -> ((model.getSecretaire_lincence() != 0) ? combRepository.find("licence", - model.getSecretaire_lincence()).count().invoke(count -> { + .call(model -> ((model.getM3_lincence() != 0) ? combRepository.find("licence", + model.getM3_lincence()).count().invoke(count -> { if (count == 0) { - throw new IllegalArgumentException("Licence secrétaire inconnue"); + throw new IllegalArgumentException("Licence membre n°3 inconnue"); } }) : Uni.createFrom().nullItem()) ).chain(model -> Panache.withTransaction(() -> repository.persist(model))) - .call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media, + .onItem().invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media, "aff_request/logo"))) - .call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, + .onItem().invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media, "aff_request/status"))) .map(__ -> "Ok"); } + + public Uni> getCurrentSaisonAffiliation() { + return Uni.createFrom().nullItem(); // TODO + } + + public Uni> getAffiliation(long id, Consumer checkPerm) { + return Uni.createFrom().nullItem(); // TODO + } + + public Uni setAffiliation(long id, AffiliationForm form) { + return Uni.createFrom().nullItem(); // TODO + } + + public Uni deleteAffiliation(long id) { + return Uni.createFrom().nullItem(); // TODO + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index 4169c52..6bb1081 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -3,12 +3,20 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.data.repository.ClubRepository; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; +import fr.titionfire.ffsaf.rest.from.FullClubForm; +import fr.titionfire.ffsaf.utils.PageResult; import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.PanacheQuery; import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; import io.quarkus.vertx.VertxContextSupport; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import org.hibernate.reactive.mutiny.Mutiny; import java.util.Collection; import java.util.List; @@ -39,4 +47,63 @@ public class ClubService { return Panache.withTransaction(() -> repository.persist(clubModel)); }); } + + public Uni> search(Integer limit, int page, String search, String country) { + if (search == null) + search = ""; + search = search + "%"; + + PanacheQuery query; + + if (country == null || country.isBlank()) + query = repository.find("name LIKE ?1", + Sort.ascending("name"), search).page(Page.ofSize(limit)); + else + query = repository.find("name LIKE ?1 AND country LIKE ?2", + Sort.ascending("name"), search, country + "%").page(Page.ofSize(limit)); + return getPageResult(query, limit, page); + } + + private Uni> getPageResult(PanacheQuery query, int limit, int page) { + return Uni.createFrom().item(new PageResult()) + .invoke(result -> result.setPage(page)) + .invoke(result -> result.setPage_size(limit)) + .call(result -> query.count().invoke(result::setResult_count)) + .call(result -> query.pageCount() + .invoke(Unchecked.consumer(pages -> { + if (page > pages) throw new BadRequestException(); + })) + .invoke(result::setPage_count)) + .call(result -> query.page(Page.of(page, limit)).list() + .map(membreModels -> membreModels.stream().map(SimpleClubModel::fromModel).toList()) + .invoke(result::setResult)); + } + + public Uni getById(long id) { + return repository.findById(id).call(m -> Mutiny.fetch(m.getContact())); + } + + public Uni getByClubId(String clubId) { + return repository.find("clubId", clubId).firstResult(); + } + + public Uni update(long id, FullClubForm input) { + /*return repository.findById(id) + .onItem().transformToUni(m -> { + m.setName(input.getName()); + m.setCountry(input.getCountry()); + m.setNo_affiliation(input.getNo_affiliation()); + m.setShieldURL(input.getShieldURL()); + return Panache.withTransaction(() -> repository.persist(m)); + });*/ + return Uni.createFrom().nullItem(); + } + + public Uni add(FullClubForm input) { + return Uni.createFrom().nullItem(); + } + + public Uni delete(long id) { + return Uni.createFrom().nullItem(); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java index e36daef..17d406c 100644 --- a/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java +++ b/src/main/java/fr/titionfire/ffsaf/net2/data/SimpleClubModel.java @@ -17,11 +17,13 @@ public class SimpleClubModel { String name; String country; String shieldURL; + String no_affiliation; public static SimpleClubModel fromModel(ClubModel model) { if (model == null) return null; - return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), model.getShieldURL()); + return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), model.getShieldURL(), + model.getNo_affiliation()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java index dc2bbbc..d44c8c1 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationEndpoints.java @@ -1,29 +1,79 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.data.model.ClubModel; +import fr.titionfire.ffsaf.domain.service.AffiliationService; +import fr.titionfire.ffsaf.rest.data.SimpleAffiliation; +import fr.titionfire.ffsaf.rest.from.AffiliationForm; import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; +import fr.titionfire.ffsaf.utils.GroupeUtils; +import io.quarkus.oidc.IdToken; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.List; +import java.util.function.Consumer; @Path("api/affiliation") public class AffiliationEndpoints { + @Inject + AffiliationService service; + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + SecurityIdentity securityIdentity; + + Consumer checkPerm = Unchecked.consumer(clubModel -> { + if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(clubModel.getId(), idToken)) + throw new ForbiddenException(); + }); @POST - @Path("save") @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA) public Uni saveAffRequest(AffiliationRequestForm form) { - System.out.println(form); - return Uni.createFrom().item("OK"); - } - /*@POST - @Path("affiliation") - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Uni saveAffRequest(AffiliationRequestForm form) { - System.out.println(form); return service.save(form); - }*/ + } + + @GET + @Path("/current") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Uni> getCurrentSaisonLicenceAdmin() { + return service.getCurrentSaisonAffiliation(); + } + + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + public Uni> getLicence(@PathParam("id") long id) { + return service.getAffiliation(id, checkPerm); + } + + @POST + @Path("{id}") + @RolesAllowed("federation_admin") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni setLicence(@PathParam("id") long id, AffiliationForm form) { + return service.setAffiliation(id, form); + } + + @DELETE + @Path("{id}") + @RolesAllowed("federation_admin") + @Produces(MediaType.TEXT_PLAIN) + public Uni deleteLicence(@PathParam("id") long id) { + return service.deleteAffiliation(id); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java index 6bfa442..5868f4f 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java @@ -1,35 +1,18 @@ package fr.titionfire.ffsaf.rest; -import fr.titionfire.ffsaf.domain.service.AffiliationService; import fr.titionfire.ffsaf.rest.client.SirenService; import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot; -import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm; import io.smallrye.mutiny.Uni; -import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; -import jodd.net.MimeTypes; -import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; -import java.io.*; -import java.net.URLConnection; -import java.nio.file.Files; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; - @Path("api/asso") public class AssoEndpoints { @RestClient SirenService sirenService; - @Inject - AffiliationService service; - - @ConfigProperty(name = "upload_dir") - String media; - @GET @Path("siren/{siren}") @Produces(MediaType.APPLICATION_JSON) @@ -42,12 +25,4 @@ public class AssoEndpoints { return throwable; }); } - - @POST - @Path("affiliation") - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Uni saveAffRequest(AffiliationRequestForm form) { - return service.save(form); - } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 20aee87..7b4c918 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -1,16 +1,29 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.domain.service.ClubService; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; +import fr.titionfire.ffsaf.rest.data.SimpleClub; +import fr.titionfire.ffsaf.rest.from.FullClubForm; +import fr.titionfire.ffsaf.utils.GroupeUtils; +import fr.titionfire.ffsaf.utils.PageResult; +import fr.titionfire.ffsaf.utils.Utils; +import io.quarkus.oidc.IdToken; import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.jwt.JsonWebToken; +import java.net.URISyntaxException; import java.util.List; +import java.util.function.Consumer; @Path("api/club") public class ClubEndpoints { @@ -18,6 +31,22 @@ public class ClubEndpoints { @Inject ClubService clubService; + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + SecurityIdentity securityIdentity; + + @ConfigProperty(name = "upload_dir") + String media; + + Consumer checkPerm = Unchecked.consumer(membreModel -> { + if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getId(), + idToken)) + throw new ForbiddenException(); + }); + @GET @Path("/no_detail") @Authenticated @@ -25,4 +54,110 @@ public class ClubEndpoints { public Uni> getAll() { return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()); } + + @GET + @Path("/find") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Uni> getFindAdmin(@QueryParam("limit") Integer limit, + @QueryParam("page") Integer page, + @QueryParam("search") String search, + @QueryParam("country") String country) { + if (limit == null) + limit = 50; + if (page == null || page < 1) + page = 1; + return clubService.search(limit, page - 1, search, country); + } + + + @GET + @Path("{id}") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + @Produces(MediaType.APPLICATION_JSON) + public Uni getById(@PathParam("id") long id) { + return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel); + } + + @PUT + @Path("{id}") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni setAdminClub(@PathParam("id") long id, FullClubForm input) { + return clubService.update(id, input) + .invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out); + })).chain(() -> { + if (input.getLogo().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub" + )).invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); + })); + else + return Uni.createFrom().nullItem(); + }).chain(() -> { + if (input.getStatus().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus" + )).invoke(Unchecked.consumer(out -> { + if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out); + })); + else + return Uni.createFrom().nullItem(); + }); + } + + @POST + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Uni addAdminClub(FullClubForm input) { + return clubService.add(input) + .invoke(Unchecked.consumer(id -> { + if (id == null) throw new InternalError("Fail to create club data"); + })).call(id -> { + if (input.getLogo().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub" + )); + else + return Uni.createFrom().nullItem(); + }).call(id -> { + if (input.getStatus().length > 0) + return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus" + )); + else + return Uni.createFrom().nullItem(); + }); + } + + @DELETE + @Path("{id}") + @RolesAllowed({"federation_admin"}) + @Produces(MediaType.TEXT_PLAIN) + public Uni deleteAdminClub(@PathParam("id") long id) { + return clubService.delete(id); + } + + + @GET + @Path("{clubId}/logo") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) + public Uni getLogo(@PathParam("clubId") String clubId) { + return clubService.getByClubId(clubId).onItem().invoke(checkPerm).chain(Unchecked.function(clubModel -> { + try { + return Utils.getMediaFile((clubModel != null) ? clubModel.getId() : -1, media, "ppClub", + Uni.createFrom().nullItem()); + } catch (URISyntaxException e) { + throw new InternalError(); + } + })); + } + + @GET + @Path("{id}/status") + @RolesAllowed({"federation_admin", "club_president", "club_secretaire"}) + public Uni getStatus(@PathParam("id") long id) throws URISyntaxException { + return Utils.getMediaFile(id, media, "clubStatus", clubService.getById(id).onItem().invoke(checkPerm)); + } + } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java index d5c5dfa..b7c60f7 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/CombEndpoints.java @@ -7,7 +7,6 @@ import fr.titionfire.ffsaf.rest.from.ClubMemberForm; import fr.titionfire.ffsaf.rest.from.FullMemberForm; import fr.titionfire.ffsaf.utils.GroupeUtils; import fr.titionfire.ffsaf.utils.PageResult; -import fr.titionfire.ffsaf.utils.Pair; import fr.titionfire.ffsaf.utils.Utils; import io.quarkus.oidc.IdToken; import io.quarkus.security.Authenticated; @@ -17,7 +16,6 @@ import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.*; -import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jodd.net.MimeTypes; @@ -25,7 +23,6 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.jwt.JsonWebToken; import java.io.*; -import java.net.URI; import java.net.URISyntaxException; import java.net.URLConnection; import java.nio.file.Files; @@ -51,7 +48,8 @@ public class CombEndpoints { SecurityIdentity securityIdentity; Consumer checkPerm = Unchecked.consumer(membreModel -> { - if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken)) + if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup( + membreModel.getClub().getId(), idToken)) throw new ForbiddenException(); }); @@ -213,37 +211,7 @@ public class CombEndpoints { @Path("{id}/photo") @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) public Uni getPhoto(@PathParam("id") long id) throws URISyntaxException { - Future> future = CompletableFuture.supplyAsync(() -> { - FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); - File[] files = new File(media, "ppMembre").listFiles(filter); - if (files != null && files.length > 0) { - File file = files[0]; - try { - byte[] data = Files.readAllBytes(file.toPath()); - return new Pair<>(file, data); - } catch (IOException ignored) { - } - } - return null; - }); - - URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp"); - - return membreService.getById(id).onItem().invoke(checkPerm).chain(__ -> Uni.createFrom().future(future) - .map(filePair -> { - if (filePair == null) - return Response.temporaryRedirect(uri).build(); - - String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName()); - - Response.ResponseBuilder resp = Response.ok(filePair.getValue()); - resp.type(MediaType.APPLICATION_OCTET_STREAM); - resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length); - resp.header(HttpHeaders.CONTENT_TYPE, mimeType); - resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; "); - - return resp.build(); - })); + return Utils.getMediaFile(id, media, "ppMembre", membreService.getById(id).onItem().invoke(checkPerm)); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java new file mode 100644 index 0000000..e4621c2 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java @@ -0,0 +1,30 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.AffiliationModel; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +@RegisterForReflection +public class SimpleAffiliation { + Long id; + Long club; + int saison; + boolean validate; + + public static SimpleAffiliation fromModel(AffiliationModel model, boolean validate) { + if (model == null) + return null; + + return new SimpleAffiliationBuilder() + .id(model.getId()) + .club(model.getClub().getId()) + .saison(model.getSaison()) + .validate(validate) + .build(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java new file mode 100644 index 0000000..7941562 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java @@ -0,0 +1,53 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.ClubModel; +import fr.titionfire.ffsaf.utils.Contact; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +import java.util.Map; + +@Data +@Builder +@ToString +@AllArgsConstructor +@RegisterForReflection +public class SimpleClub { + private Long id; + private String clubId; + private String name; + private String country; + private String shieldURL; + private Map contact; + private String training_location; + private String training_day_time; + private String contact_intern; + private String RNA; + private String SIRET; + private String no_affiliation; + private boolean international; + + public static SimpleClub fromModel(ClubModel model) { + if (model == null) + return null; + + return new SimpleClubBuilder() + .id(model.getId()) + .clubId(model.getClubId()) + .name(model.getName()) + .country(model.getCountry()) + .shieldURL(model.getShieldURL()) + .contact(model.getContact()) + .training_location(model.getTraining_location()) + .training_day_time(model.getTraining_day_time()) + .contact_intern(model.getContact_intern()) + .RNA(model.getRNA()) + .SIRET(model.getSIRET()) + .no_affiliation(model.getNo_affiliation()) + .international(model.isInternational()) + .build(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationForm.java new file mode 100644 index 0000000..cfda16d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationForm.java @@ -0,0 +1,4 @@ +package fr.titionfire.ffsaf.rest.from; + +public class AffiliationForm { +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java index 04f002d..2657a71 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -1,6 +1,7 @@ package fr.titionfire.ffsaf.rest.from; import fr.titionfire.ffsaf.data.model.AffiliationRequestModel; +import fr.titionfire.ffsaf.utils.RoleAsso; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.core.MediaType; import lombok.Getter; @@ -14,7 +15,7 @@ public class AffiliationRequestForm { private String name = null; @FormParam("siren") - private String siren = null; + private Long siren = null; @FormParam("rna") private String rna = null; @@ -30,32 +31,38 @@ public class AffiliationRequestForm { @PartType(MediaType.APPLICATION_OCTET_STREAM) private byte[] logo = new byte[0]; - @FormParam("president-nom") - private String president_lname = null; - @FormParam("president-prenom") - private String president_fname = null; - @FormParam("president-mail") - private String president_email = null; - @FormParam("president-licence") - private String president_lincence = null; + @FormParam("m1_nom") + private String m1_lname = null; + @FormParam("m1_prenom") + private String m1_fname = null; + @FormParam("m1_mail") + private String m1_email = null; + @FormParam("m1_licence") + private String m1_lincence = null; + @FormParam("m1_role") + private RoleAsso m1_role = null; - @FormParam("tresorier-nom") - private String tresorier_lname = null; - @FormParam("tresorier-prenom") - private String tresorier_fname = null; - @FormParam("tresorier-mail") - private String tresorier_email = null; - @FormParam("tresorier-licence") - private String tresorier_lincence = null; + @FormParam("m2_nom") + private String m2_lname = null; + @FormParam("m2_prenom") + private String m2_fname = null; + @FormParam("m2_mail") + private String m2_email = null; + @FormParam("m2_licence") + private String m2_lincence = null; + @FormParam("m2_role") + private RoleAsso m2_role = null; - @FormParam("secretaire-nom") - private String secretaire_lname = null; - @FormParam("secretaire-prenom") - private String secretaire_fname = null; - @FormParam("secretaire-mail") - private String secretaire_email = null; - @FormParam("secretaire-licence") - private String secretaire_lincence = null; + @FormParam("m3_nom") + private String m3_lname = null; + @FormParam("m3_prenom") + private String m3_fname = null; + @FormParam("m3_mail") + private String m3_email = null; + @FormParam("m3_licence") + private String m3_lincence = null; + @FormParam("m3_role") + private RoleAsso m3_role = null; public AffiliationRequestModel toModel() { AffiliationRequestModel model = new AffiliationRequestModel(); @@ -64,23 +71,26 @@ public class AffiliationRequestForm { model.setRNA(this.getRna()); model.setAddress(this.getAdresse()); - model.setPresident_lname(this.getPresident_lname()); - model.setPresident_fname(this.getPresident_fname()); - model.setPresident_email(this.getPresident_email()); - model.setPresident_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank()) - ? 0 : Integer.parseInt(this.getPresident_lincence())); + model.setM1_lname(this.getM1_lname()); + model.setM1_fname(this.getM1_fname()); + model.setM1_email(this.getM1_email()); + model.setM1_lincence((this.getM1_lincence() == null || this.getM1_lincence().isBlank()) + ? 0 : Integer.parseInt(this.getM1_lincence())); + model.setM1_role(this.getM1_role()); - model.setTresorier_lname(this.getTresorier_lname()); - model.setTresorier_fname(this.getTresorier_fname()); - model.setTresorier_email(this.getTresorier_email()); - model.setTresorier_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank()) - ? 0 : Integer.parseInt(this.getTresorier_lincence())); + model.setM2_lname(this.getM2_lname()); + model.setM2_fname(this.getM2_fname()); + model.setM2_email(this.getM2_email()); + model.setM2_lincence((this.getM1_lincence() == null || this.getM1_lincence().isBlank()) + ? 0 : Integer.parseInt(this.getM2_lincence())); + model.setM2_role(this.getM2_role()); - model.setSecretaire_lname(this.getSecretaire_lname()); - model.setSecretaire_fname(this.getSecretaire_fname()); - model.setSecretaire_email(this.getSecretaire_email()); - model.setSecretaire_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank()) - ? 0 : Integer.parseInt(this.getSecretaire_lincence())); + model.setM3_lname(this.getM3_lname()); + model.setM3_fname(this.getM3_fname()); + model.setM3_email(this.getM3_email()); + model.setM3_lincence((this.getM1_lincence() == null || this.getM1_lincence().isBlank()) + ? 0 : Integer.parseInt(this.getM3_lincence())); + model.setM3_role(this.getM3_role()); return model; } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java new file mode 100644 index 0000000..fcd9108 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java @@ -0,0 +1,23 @@ +package fr.titionfire.ffsaf.rest.from; + +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.core.MediaType; +import lombok.Getter; +import org.jboss.resteasy.reactive.PartType; + +@Getter +public class FullClubForm { + @FormParam("id") + private String id = null; + + @FormParam("name") + private String name = null; + + @FormParam("status") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + private byte[] status = new byte[0]; + + @FormParam("logo") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + private byte[] logo = new byte[0]; +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java index 84991b1..c591150 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java @@ -5,9 +5,13 @@ import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection public enum RoleAsso { MEMBRE("Membre", 0), - PRESIDENT("Président", 3), - TRESORIER("Trésorier", 1), - SECRETAIRE("Secrétaire", 2); + PRESIDENT("Président", 7), + VPRESIDENT("Vise-Président", 6), + SECRETAIRE("Secrétaire", 5), + VSECRETAIRE("Vise-Secrétaire", 4), + TRESORIER("Trésorier", 3), + VTRESORIER("Vise-Trésorier", 2), + MEMBREBUREAU("Membre bureau", 1); public final String name; public final int level; diff --git a/src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java b/src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java new file mode 100644 index 0000000..def85ee --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/StringSimilarity.java @@ -0,0 +1,65 @@ +package fr.titionfire.ffsaf.utils; + +public class StringSimilarity { + + public static int similarity(String s1, String s2) { + String longer = s1, shorter = s2; + if (s1.length() < s2.length()) { + longer = s2; + shorter = s1; + } + int longerLength = longer.length(); + if (longerLength == 0) { + return 1; + } + return editDistance(longer, shorter); + + } + + public static int editDistance(String s1, String s2) { + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + int[] costs = new int[s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + int lastValue = i; + for (int j = 0; j <= s2.length(); j++) { + if (i == 0) + costs[j] = j; + else { + if (j > 0) { + int newValue = costs[j - 1]; + if (s1.charAt(i - 1) != s2.charAt(j - 1)) + newValue = Math.min(Math.min(newValue, lastValue), + costs[j]) + 1; + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) + costs[s2.length()] = lastValue; + } + return costs[s2.length()]; + } + + public static void printSimilarity(String s, String t) { + System.out.printf( + "%d is the similarity between \"%s\" and \"%s\"%n", similarity(s, t), s, t); + } + + /*public static void main(String[] args) { + printSimilarity("Xavier Login", "Xavier Lojin"); + printSimilarity("Xavier Login", "Xavier ogin"); + printSimilarity("Xavier Login", "avier Login"); + printSimilarity("Xavier Login", "xavier login"); + printSimilarity("Xavier Login", "Xaviér Login"); + printSimilarity("Xavier Gomme Login", "Xavier Login"); + printSimilarity("Xavier Login", "Xavier Gomme Login"); + printSimilarity("Xavier Login", "Xavier"); + printSimilarity("Xavier Login", "Login"); + printSimilarity("Jule", "Julles"); + printSimilarity("Xavier", "Xaviér"); + printSimilarity("Xavier", "xavvie"); + }*/ +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index 88c4925..e6de816 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -1,8 +1,19 @@ package fr.titionfire.ffsaf.utils; +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import jodd.net.MimeTypes; +import net.sf.jmimemagic.Magic; +import net.sf.jmimemagic.MagicException; +import net.sf.jmimemagic.MagicMatchNotFoundException; +import net.sf.jmimemagic.MagicParseException; +import org.jboss.logging.Logger; import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URLConnection; import java.nio.file.Files; import java.util.Calendar; @@ -11,6 +22,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; public class Utils { + private static final org.jboss.logging.Logger LOGGER = Logger.getLogger(Utils.class); public static int getSaison() { return getSaison(new Date()); @@ -30,7 +42,12 @@ public class Utils { public static Future replacePhoto(long id, byte[] input, String media, String dir) { return CompletableFuture.supplyAsync(() -> { try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) { - String mimeType = URLConnection.guessContentTypeFromStream(is); + String mimeType; + try { + mimeType = Magic.getMagicMatch(input, false).getMimeType(); + } catch (MagicParseException | MagicMatchNotFoundException | MagicException e) { + mimeType = URLConnection.guessContentTypeFromStream(is); + } String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false); if (detectedExtensions.length == 0) throw new IOException("Fail to detect file extension for MIME type " + mimeType); @@ -57,4 +74,39 @@ public class Utils { } }); } + + public static Uni getMediaFile(long id, String media, String dirname, + Uni uniBase) throws URISyntaxException { + Future> future = CompletableFuture.supplyAsync(() -> { + FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id)); + File[] files = new File(media, dirname).listFiles(filter); + if (files != null && files.length > 0) { + File file = files[0]; + try { + byte[] data = Files.readAllBytes(file.toPath()); + return new Pair<>(file, data); + } catch (IOException ignored) { + } + } + return null; + }); + + URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp"); + + return uniBase.chain(__ -> Uni.createFrom().future(future) + .map(filePair -> { + if (filePair == null) + return Response.temporaryRedirect(uri).build(); + + String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName()); + + Response.ResponseBuilder resp = Response.ok(filePair.getValue()); + resp.type(MediaType.APPLICATION_OCTET_STREAM); + resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length); + resp.header(HttpHeaders.CONTENT_TYPE, mimeType); + resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; "); + + return resp.build(); + })); + } } diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 97bbfb1..95678f6 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -14,6 +14,10 @@ integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" /> + + diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 37c5149..7e63a4c 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -15,8 +15,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.21.2", "react-toastify": "^10.0.4" @@ -1023,6 +1025,16 @@ "node": ">= 8" } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", @@ -3113,6 +3125,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3558,6 +3575,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-loader-spinner": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 534a96f..39798b9 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -17,8 +17,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.21.2", "react-toastify": "^10.0.4" diff --git a/src/main/webapp/src/components/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx index 6a00bce..fff6053 100644 --- a/src/main/webapp/src/components/MemberCustomFiels.jsx +++ b/src/main/webapp/src/components/MemberCustomFiels.jsx @@ -36,7 +36,7 @@ export function BirthDayField({inti_date, inti_category}) { } -export function OptionField({name, text, values, value, disabled=false}) { +export function OptionField({name, text, values, value, disabled = false}) { return
@@ -49,12 +49,20 @@ export function OptionField({name, text, values, value, disabled=false}) {
} -export function TextField({name, text, value, placeholder, type = "text"}) { +export function CountryList({name, text, value, values = undefined, disabled = false}) { + if (values === undefined){ + values = {NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'} + } + + return +} + +export function TextField({name, text, value, placeholder, type = "text", disabled = false}) { return
{text} + name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required/>
} @@ -79,7 +87,7 @@ export function CheckField({name, text, value, row = false}) { } -export const Checkbox = ({ label, value, onChange }) => { +export const Checkbox = ({label, value, onChange}) => { const handleChange = () => { onChange(!value); }; diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 3e5c6fb..382731e 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -72,7 +72,7 @@ function AdminMenu() {
  • Member
  • -
  • B
  • +
  • Club
} diff --git a/src/main/webapp/src/components/SearchBar.jsx b/src/main/webapp/src/components/SearchBar.jsx new file mode 100644 index 0000000..6140436 --- /dev/null +++ b/src/main/webapp/src/components/SearchBar.jsx @@ -0,0 +1,43 @@ +import {useEffect, useState} from "react"; + +const removeDiacritics = str => { + return str + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') +} + + +export function SearchBar({search}) { + const [searchInput, setSearchInput] = useState(""); + + const handelChange = (e) => { + setSearchInput(e.target.value); + } + + const handleKeyDown = (event) => { + if (event.key === 'Enter') { + searchMember(); + } + } + + const searchMember = () => { + search(removeDiacritics(searchInput)); + } + + useEffect(() => { + const delayDebounceFn = setTimeout(() => { + searchMember(); + }, 750) + return () => clearTimeout(delayDebounceFn) + }, [searchInput]) + + return
+
+ + +
+
+} \ No newline at end of file diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index b8f92b7..b07b9d4 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -30,15 +30,16 @@ export function DemandeAff() { const submit = (event) => { event.preventDefault() const formData = new FormData(event.target) + formData.append("m1_role", event.target.m1_role?.value) toast.promise( - apiAxios.post(`asso/affiliation`, formData, { headers: {'Accept': '*/*'}}), + apiAxios.post(`/affiliation`, formData, {headers: {'Accept': '*/*'}}), { pending: "Enregistrement de la demande d'affiliation en cours", success: "Demande d'affiliation enregistrée avec succès 🎉", error: "Échec de la demande d'affiliation 😕" } ).then(_ => { - // navigate("/affiliation/ok") + navigate("/affiliation/ok") }) } @@ -67,14 +68,14 @@ export function DemandeAff() {

L'association

-

Le président

- -

Le trésorier

- -

Le secrétaire

- +

Membre n°1

+ +

Membre n°2

+ +

Membre n°3

+ -
+

Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF

Notez que pour finaliser votre affiliation, il vous faudra : @@ -126,13 +127,13 @@ function AssoInfo() { Nom de l'association* + aria-describedby="basic-addon1" required defaultValue="Mesnie"/>
N° SIREN* setSiren(e.target.value)}/> + onChange={e => setSiren(e.target.value)} defaultValue={500213731}/> @@ -173,36 +174,65 @@ function AssoInfo() { } function MembreInfo({role}) { - return
-
-
- - + const [switchOn, setSwitchOn] = useState(false); + + return <> +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
-
-
- - + +
+ +
+ setSwitchOn(!switchOn)}/>
-
-
- - + {switchOn && +
+
+ + +
-
-
-
OU
-
- - -
-
-
+ } + + } export function DemandeAffOk() { diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx index 8fbd771..98e8d95 100644 --- a/src/main/webapp/src/pages/MemberList.jsx +++ b/src/main/webapp/src/pages/MemberList.jsx @@ -9,6 +9,7 @@ import {Checkbox} from "../components/MemberCustomFiels.jsx"; import axios from "axios"; import {apiAxios} from "../utils/Tools.js"; import {toast} from "react-toastify"; +import {SearchBar} from "../components/SearchBar.jsx"; const removeDiacritics = str => { return str @@ -106,41 +107,6 @@ export function MemberList({source}) { } -function SearchBar({search}) { - const [searchInput, setSearchInput] = useState(""); - - const handelChange = (e) => { - setSearchInput(e.target.value); - } - - const handleKeyDown = (event) => { - if (event.key === 'Enter') { - searchMember(); - } - } - - const searchMember = () => { - search(removeDiacritics(searchInput)); - } - - useEffect(() => { - const delayDebounceFn = setTimeout(() => { - searchMember(); - }, 750) - return () => clearTimeout(delayDebounceFn) - }, [searchInput]) - - return
-
- - -
-
-} - function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page}) { const pages = [] for (let i = 1; i <= data.page_count; i++) { diff --git a/src/main/webapp/src/pages/admin/AdminRoot.jsx b/src/main/webapp/src/pages/admin/AdminRoot.jsx index e2860ed..063c17e 100644 --- a/src/main/webapp/src/pages/admin/AdminRoot.jsx +++ b/src/main/webapp/src/pages/admin/AdminRoot.jsx @@ -4,6 +4,10 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx"; import {MemberList} from "../MemberList.jsx"; import {MemberPage} from "./member/MemberPage.jsx"; import {NewMemberPage} from "./member/NewMemberPage.jsx"; +import {ClubList} from "./club/ClubList.jsx"; +import {AffiliationReqPage} from "./affiliation/AffiliationReqPage.jsx"; +import {NewClubPage} from "./club/NewClubPage.jsx"; +import {ClubPage} from "./club/ClubPage.jsx"; export function AdminRoot() { return <> @@ -28,6 +32,22 @@ export function getAdminChildren() { path: 'member/new', element: }, + { + path: 'club', + element: + }, + { + path: 'club/:id', + element: + }, + { + path: 'affiliation/request', + element: + }, + { + path: 'club/new', + element: + }, { path: 'b', element:
Admin B
diff --git a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx new file mode 100644 index 0000000..443e68e --- /dev/null +++ b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx @@ -0,0 +1,16 @@ +import {useNavigate} from "react-router-dom"; + +export function AffiliationReqPage() { + const navigate = useNavigate(); + + return <> +

Page affiliation

+ +
+
+
+
+ +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/club/AffiliationCard.jsx b/src/main/webapp/src/pages/admin/club/AffiliationCard.jsx new file mode 100644 index 0000000..e392e9c --- /dev/null +++ b/src/main/webapp/src/pages/admin/club/AffiliationCard.jsx @@ -0,0 +1,187 @@ +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {useEffect, useReducer, useState} from "react"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faPen} from "@fortawesome/free-solid-svg-icons"; +import {AxiosError} from "../../../components/AxiosError.jsx"; +import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {apiAxios, getSaison} from "../../../utils/Tools.js"; +import {Input} from "../../../components/Input.jsx"; +import {toast} from "react-toastify"; + +function affiliationReducer(affiliation, action) { + switch (action.type) { + case 'ADD': + return [ + ...affiliation, + action.payload + ] + case 'REMOVE': + return affiliation.filter(affiliation => affiliation.id !== action.payload) + case 'UPDATE_OR_ADD': + const index = affiliation.findIndex(affiliation => affiliation.id === action.payload.id) + if (index === -1) { + return [ + ...affiliation, + action.payload + ] + } else { + affiliation[index] = action.payload + return [...affiliation] + } + case 'SORT': + return affiliation.sort((a, b) => b.saison - a.saison) + default: + throw new Error() + } +} + +export function AffiliationCard({clubData}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1) + + const [modalAffiliation, setModal] = useState({id: -1, club: clubData.id}) + const [affiliations, dispatch] = useReducer(affiliationReducer, []) + + useEffect(() => { + if (!data) return + for (const dataKey of data) { + dispatch({type: 'UPDATE_OR_ADD', payload: dataKey}) + } + dispatch({type: 'SORT'}) + }, [data]); + + return
+
+
+
Affiliation
+
+ +
+
+
+
+
    + {affiliations.map((affiliation, index) => { + return
    +
    {affiliation?.saison}-{affiliation?.saison + 1}
    + +
    + })} + {error && } +
+
+ + +
; +} + +function sendAffiliation(event, dispatch) { + event.preventDefault(); + + const formData = new FormData(event.target); + toast.promise( + apiAxios.post(`/affiliation/${formData.get('membre')}`, formData), // TODO + { + pending: "Enregistrement de l'affiliation en cours", + success: "Affiliation enregistrée avec succès 🎉", + error: "Échec de l'enregistrement de l'affiliation 😕" + } + ).then(data => { + dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) + dispatch({type: 'SORT'}) + }) + +} + +function removeAffiliation(id, dispatch) { + toast.promise( + apiAxios.delete(`/affiliation/${id}`), + { + pending: "Suppression de l'affiliation en cours", + success: "Affiliation supprimée avec succès 🎉", + error: "Échec de la suppression de l'affiliation 😕" + } + ).then(_ => { + dispatch({type: 'REMOVE', payload: id}) + }) +} + +function ModalContent({affiliation, dispatch}) { + const [saison, setSaison] = useState(0) + const [validate, setValidate] = useState(false) + const [isNew, setNew] = useState(true) + const setSeason = (event) => { + setSaison(Number(event.target.value)) + } + const handleValidateChange = (event) => { + setValidate(event.target.value === 'true'); + } + + useEffect(() => { + if (affiliation.id !== -1) { + setNew(false) + setSaison(affiliation.saison) + setValidate(affiliation.validate) + } else { + setNew(true) + setSaison(getSaison()) + setValidate(false) + } + }, [affiliation]); + + return
sendAffiliation(e, dispatch)}> + + +
+

Edition de l'affiliation

+ +
+
+
+ {isNew + ? + : <>{saison} + } + - + {saison + 1} +
+ +
+
+ + + {isNew || } +
+
+} + +function RadioGroupeOnOff({value, onChange, name, text}) { + return
+ {text} + + + + +
; +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/club/ClubList.jsx b/src/main/webapp/src/pages/admin/club/ClubList.jsx new file mode 100644 index 0000000..80c0e9b --- /dev/null +++ b/src/main/webapp/src/pages/admin/club/ClubList.jsx @@ -0,0 +1,192 @@ +import {useLocation, useNavigate} from "react-router-dom"; +import {useEffect, useState} from "react"; +import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {toast} from "react-toastify"; +import {apiAxios} from "../../../utils/Tools.js"; +import {AxiosError} from "../../../components/AxiosError.jsx"; +import {Checkbox} from "../../../components/MemberCustomFiels.jsx"; +import {ThreeDots} from "react-loader-spinner"; +import {SearchBar} from "../../../components/SearchBar.jsx"; + +export function ClubList() { + const {hash} = useLocation(); + const navigate = useNavigate(); + let page = Number(hash.substring(1)); + page = (page > 0) ? page : 1; + + const [clubData, setClubData] = useState([]); + const [affiliationData, setAffiliationData] = useState([]); + const [showAffiliationState, setShowAffiliationState] = useState(false); + const [countryFilter, setCountryFilter] = useState(""); + const [lastSearch, setLastSearch] = useState(""); + + const setLoading = useLoadingSwitcher() + const {data, error, refresh} = useFetch(`/club/find?page=${page}`, setLoading, 1) + + + useEffect(() => { + refresh(`/club/find?page=${page}&search=${lastSearch}&country=${countryFilter}`); + }, [hash, countryFilter]); + + useEffect(() => { + if (!data) + return; + const data2 = []; + for (const e of data.result) { + data2.push({ + id: e.id, + name: e.name, + country: e.country, + shieldURL: e.shieldURL, + no_affiliation: e.no_affiliation, + affiliation: showAffiliationState ? affiliationData.find(licence => licence.club === e.id) : null + }) + } + setClubData(data2); + }, [data, affiliationData]); + + useEffect(() => { + if (!showAffiliationState) + return; + + toast.promise( + apiAxios.get(`/affiliation/current`), + { + pending: "Chargement des affiliation...", + success: "Affiliation chargées", + error: "Impossible de charger les affiliations" + }) + .then(data => { + setAffiliationData(data.data); + }); + }, [showAffiliationState]); + + const search = (search) => { + if (search === lastSearch) + return; + setLastSearch(search); + refresh(`/club/find?page=${page}&search=${search}&country=${countryFilter}`); + } + + return <> +

Club

+
+
+
+ + {data + ? + : error + ? + : + } +
+
+
+ + +
+
+
Filtre
+
+ +
+
+
+
+
+ +} + +function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, page}) { + const pages = [] + for (let i = 1; i <= data.page_count; i++) { + pages.push(
  • + navigate("#" + i)}>{i} +
  • ); + } + + return <> +
    + Ligne {((page - 1) * data.page_size) + 1} à { + (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size)} (page {page} sur {data.page_count}) +
    + {visibleclub.map(club => ())} +
    +
    +
    + +
    + +} + +function MakeRow({club, showAffiliationState, navigate}) { + const rowContent = <> +
    + {String(club.no_affiliation).padStart(5, '0')} +
    +
    {club.name}
    +
    +
    + {club.country} + + + if (showAffiliationState && club.affiliation != null) { + return
    navigate("" + club.id)}>{rowContent}
    + } else { + return
    navigate("" + club.id)}> + {rowContent} +
    + } +} + +let allCountry = [] + +function FiltreBar({showAffiliationState, setShowAffiliationState, data, countryFilter, setCountryFilter}) { + useEffect(() => { + if (!data) + return; + allCountry.push(...data.result.map((e) => e.club?.name)) + allCountry = allCountry.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() + }, [data]); + + return
    +
    + +
    +
    + +
    +
    +} + +function Def() { + return
    +
  • +
  • +
  • +
  • +
  • +
    +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/club/ClubPage.jsx b/src/main/webapp/src/pages/admin/club/ClubPage.jsx new file mode 100644 index 0000000..0269304 --- /dev/null +++ b/src/main/webapp/src/pages/admin/club/ClubPage.jsx @@ -0,0 +1,128 @@ +import {useNavigate, useParams} from "react-router-dom"; +import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; +import {useFetch} from "../../../hooks/useFetch.js"; +import {toast} from "react-toastify"; +import {apiAxios} from "../../../utils/Tools.js"; +import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; +import {AxiosError} from "../../../components/AxiosError.jsx"; +import {AffiliationCard} from "./AffiliationCard.jsx"; +import {CheckField, CountryList, TextField} from "../../../components/MemberCustomFiels.jsx"; + +import {MapContainer, Marker, Popup, TileLayer, useMap} from 'react-leaflet' + +const vite_url = import.meta.env.VITE_URL; + +export function ClubPage() { + const {id} = useParams() + const navigate = useNavigate(); + + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/club/${id}`, setLoading, 1) + + const handleRm = () => { + toast.promise( + apiAxios.delete(`/club/${id}`), + { + pending: "Suppression du club en cours...", + success: "Club supprimé avec succès 🎉", + error: "Échec de la suppression du club 😕" + } + ).then(_ => { + navigate("/admin/club") + }) + } + + return <> +

    Page membre

    + + {data + ?
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    +
    + : error && + } + +} + +function InformationForm({data}) { + return
    +
    Licence n°{data.no_affiliation}
    +
    + + + + + + + avatar + +
    +
    + + +
    +
    Laissez vide pour ne rien changer.
    +
    + + + + + + + + +
    +
    ; +} + // https://annuaire-entreprises.data.gouv.fr/entreprise/la-mesnie-des-chevaliers-de-st-georges-et-de-st-michel-500213731 +const position = [51.505, -0.09] +function MainMap() { + function handleReturnCurrentPosition() { + console.log("I have clicked return button!!"); + //const newCurrentPositionId = uuidv4(); + //setReturnCurrentPosition(newCurrentPositionId); + //console.log(newCurrentPositionId); + } + + return ( + <> + + + + + A pretty CSS3 popup.
    Easily customizable. +
    +
    +
    + + + + ) +} + +function SearchBarMap() { + return <> + +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/club/NewClubPage.jsx b/src/main/webapp/src/pages/admin/club/NewClubPage.jsx new file mode 100644 index 0000000..20fe771 --- /dev/null +++ b/src/main/webapp/src/pages/admin/club/NewClubPage.jsx @@ -0,0 +1,16 @@ +import {useNavigate} from "react-router-dom"; + +export function NewClubPage() { + const navigate = useNavigate(); + + return <> +

    Page affiliation

    + +
    +
    +
    +
    + +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/member/InformationForm.jsx b/src/main/webapp/src/pages/admin/member/InformationForm.jsx index 21545b0..3c62e9c 100644 --- a/src/main/webapp/src/pages/admin/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/admin/member/InformationForm.jsx @@ -2,7 +2,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {apiAxios} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; import imageCompression from "browser-image-compression"; -import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; import {ClubSelect} from "../../../components/ClubSelect.jsx"; export function addPhoto(event, formData, send) { @@ -74,8 +74,7 @@ export function InformationForm({data}) { type="email"/> - +
    @@ -86,7 +85,11 @@ export function InformationForm({data}) { MEMBRE: 'Membre', PRESIDENT: 'Président', TRESORIER: 'Trésorier', - SECRETAIRE: 'Secrétaire' + SECRETAIRE: 'Secrétaire', + VPRESIDENT: 'Vise-Président', + VTRESORIER: 'Vise-Trésorier', + VSECRETAIRE: 'Vise-Secrétaire', + MEMBREBUREAU: 'Membre bureau' }}/> diff --git a/src/main/webapp/src/pages/club/member/InformationForm.jsx b/src/main/webapp/src/pages/club/member/InformationForm.jsx index 404c078..7d9f96f 100644 --- a/src/main/webapp/src/pages/club/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/club/member/InformationForm.jsx @@ -3,7 +3,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {apiAxios} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; -import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; +import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; import {addPhoto} from "../../admin/member/InformationForm.jsx"; export function InformationForm({data}) { @@ -52,8 +52,7 @@ export function InformationForm({data}) { type="email"/> - +