commit
d7882b5bff
5
pom.xml
5
pom.xml
@ -104,6 +104,11 @@
|
||||
<artifactId>jodd-util</artifactId>
|
||||
<version>6.2.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "affiliation_request")
|
||||
public class AffiliationRequestModel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
String name;
|
||||
String siren;
|
||||
String RNA;
|
||||
String address;
|
||||
|
||||
String president_lname;
|
||||
String president_fname;
|
||||
String president_email;
|
||||
int president_lincence;
|
||||
|
||||
String tresorier_lname;
|
||||
String tresorier_fname;
|
||||
String tresorier_email;
|
||||
int tresorier_lincence;
|
||||
|
||||
String secretaire_lname;
|
||||
String secretaire_fname;
|
||||
String secretaire_email;
|
||||
int secretaire_lincence;
|
||||
|
||||
int saison;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package fr.titionfire.ffsaf.data.repository;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AffiliationRequestRepository implements PanacheRepositoryBase<AffiliationRequestModel, Long> {
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
||||
import fr.titionfire.ffsaf.data.repository.AffiliationRequestRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
||||
import fr.titionfire.ffsaf.utils.Utils;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
public class AffiliationService {
|
||||
|
||||
@Inject
|
||||
CombRepository combRepository;
|
||||
|
||||
@Inject
|
||||
AffiliationRequestRepository repository;
|
||||
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
public Uni<String> save(AffiliationRequestForm form) {
|
||||
AffiliationRequestModel affModel = form.toModel();
|
||||
affModel.setSaison(Utils.getSaison());
|
||||
|
||||
return Uni.createFrom().item(affModel)
|
||||
.call(model -> ((model.getPresident_lincence() != 0) ? combRepository.find("licence",
|
||||
model.getPresident_lincence()).count().invoke(count -> {
|
||||
if (count == 0) {
|
||||
throw new IllegalArgumentException("Licence président inconnue");
|
||||
}
|
||||
}) : Uni.createFrom().nullItem())
|
||||
)
|
||||
.call(model -> ((model.getTresorier_lincence() != 0) ? combRepository.find("licence",
|
||||
model.getTresorier_lincence()).count().invoke(count -> {
|
||||
if (count == 0) {
|
||||
throw new IllegalArgumentException("Licence trésorier inconnue");
|
||||
}
|
||||
}) : Uni.createFrom().nullItem())
|
||||
)
|
||||
.call(model -> ((model.getSecretaire_lincence() != 0) ? combRepository.find("licence",
|
||||
model.getSecretaire_lincence()).count().invoke(count -> {
|
||||
if (count == 0) {
|
||||
throw new IllegalArgumentException("Licence secrétaire inconnue");
|
||||
}
|
||||
}) : Uni.createFrom().nullItem())
|
||||
).chain(model -> Panache.withTransaction(() -> repository.persist(model)))
|
||||
.call(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,
|
||||
"aff_request/status")))
|
||||
.map(__ -> "Ok");
|
||||
}
|
||||
}
|
||||
@ -195,6 +195,17 @@ public class KeycloakService {
|
||||
return membreService.setUserId(id, nid).map(__ -> "OK");
|
||||
}
|
||||
|
||||
public Uni<?> removeAccount(String userId) {
|
||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
try (Response response = keycloak.realm(realm).users().delete(userId)) {
|
||||
System.out.println(response.getStatusInfo());
|
||||
if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT))
|
||||
throw new KeycloakException("Fail to delete user %s (reason=%s)".formatted(userId, response.getStatusInfo().getReasonPhrase()));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<UserRepresentation> getUser(String username) {
|
||||
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
|
||||
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.data.repository.ClubRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
||||
import fr.titionfire.ffsaf.net2.ServerCustom;
|
||||
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
|
||||
import fr.titionfire.ffsaf.net2.request.SReqComb;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
||||
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.RoleAsso;
|
||||
import fr.titionfire.ffsaf.utils.*;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
@ -37,6 +36,9 @@ public class MembreService {
|
||||
CombRepository repository;
|
||||
@Inject
|
||||
ClubRepository clubRepository;
|
||||
@Inject
|
||||
LicenceRepository licenceRepository;
|
||||
|
||||
@Inject
|
||||
ServerCustom serverCustom;
|
||||
@Inject
|
||||
@ -163,10 +165,74 @@ public class MembreService {
|
||||
.map(__ -> "OK");
|
||||
}
|
||||
|
||||
public Uni<Long> add(FullMemberForm input) {
|
||||
return clubRepository.findById(input.getClub())
|
||||
.chain(clubModel -> {
|
||||
MembreModel model = getMembreModel(input, clubModel);
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
})
|
||||
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
||||
.map(MembreModel::getId);
|
||||
}
|
||||
|
||||
public Uni<Long> add(FullMemberForm input, String subject) {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.chain(membreModel -> {
|
||||
MembreModel model = getMembreModel(input, membreModel.getClub());
|
||||
model.setRole(RoleAsso.MEMBRE);
|
||||
model.setGrade_arbitrage(GradeArbitrage.NA);
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
})
|
||||
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
||||
.map(MembreModel::getId);
|
||||
}
|
||||
|
||||
public Uni<String> delete(long id) {
|
||||
return repository.findById(id)
|
||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
||||
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
|
||||
.call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel)))
|
||||
.invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id))
|
||||
.map(__ -> "Ok");
|
||||
}
|
||||
|
||||
public Uni<String> delete(long id, JsonWebToken idToken) {
|
||||
return repository.findById(id)
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
||||
throw new ForbiddenException();
|
||||
}))
|
||||
.call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count()
|
||||
.invoke(Unchecked.consumer(l -> {
|
||||
if (l > 0)
|
||||
throw new BadRequestException();
|
||||
})))
|
||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
||||
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
|
||||
.call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel)))
|
||||
.invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id))
|
||||
.map(__ -> "Ok");
|
||||
}
|
||||
|
||||
public Uni<?> setUserId(Long id, String id1) {
|
||||
return repository.findById(id).chain(membreModel -> {
|
||||
membreModel.setUserId(id1);
|
||||
return Panache.withTransaction(() -> repository.persist(membreModel));
|
||||
});
|
||||
}
|
||||
|
||||
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
|
||||
MembreModel model = new MembreModel();
|
||||
model.setFname(input.getFname());
|
||||
model.setLname(input.getLname());
|
||||
model.setEmail(input.getEmail());
|
||||
model.setGenre(input.getGenre());
|
||||
model.setCountry(input.getCountry());
|
||||
model.setBirth_date(input.getBirth_date());
|
||||
model.setCategorie(input.getCategorie());
|
||||
model.setClub(clubModel);
|
||||
model.setRole(input.getRole());
|
||||
model.setGrade_arbitrage(input.getGrade_arbitrage());
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
|
||||
@Path("api/affiliation")
|
||||
public class AffiliationEndpoints {
|
||||
|
||||
|
||||
|
||||
@POST
|
||||
@Path("save")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Uni<String> 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<String> saveAffRequest(AffiliationRequestForm form) {
|
||||
System.out.println(form);
|
||||
return service.save(form);
|
||||
}*/
|
||||
}
|
||||
@ -1,18 +1,35 @@
|
||||
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)
|
||||
@ -25,4 +42,12 @@ public class AssoEndpoints {
|
||||
return throwable;
|
||||
});
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("affiliation")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
|
||||
return service.save(form);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ 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;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
@ -58,8 +59,10 @@ public class CombEndpoints {
|
||||
@Path("/find/admin")
|
||||
@RolesAllowed({"federation_admin"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Uni<PageResult<SimpleMembre>> getFindAdmin(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page,
|
||||
@QueryParam("search") String search, @QueryParam("club") String club) {
|
||||
public Uni<PageResult<SimpleMembre>> getFindAdmin(@QueryParam("limit") Integer limit,
|
||||
@QueryParam("page") Integer page,
|
||||
@QueryParam("search") String search,
|
||||
@QueryParam("club") String club) {
|
||||
if (limit == null)
|
||||
limit = 50;
|
||||
if (page == null || page < 1)
|
||||
@ -71,7 +74,8 @@ public class CombEndpoints {
|
||||
@Path("/find/club")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Uni<PageResult<SimpleMembre>> getFindClub(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page,
|
||||
public Uni<PageResult<SimpleMembre>> getFindClub(@QueryParam("limit") Integer limit,
|
||||
@QueryParam("page") Integer page,
|
||||
@QueryParam("search") String search) {
|
||||
if (limit == null)
|
||||
limit = 50;
|
||||
@ -88,7 +92,7 @@ public class CombEndpoints {
|
||||
return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel);
|
||||
}
|
||||
|
||||
@POST
|
||||
@PUT
|
||||
@Path("{id}")
|
||||
@RolesAllowed({"federation_admin"})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@ -99,7 +103,8 @@ public class CombEndpoints {
|
||||
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
||||
})).chain(() -> {
|
||||
if (input.getPhoto_data().length > 0)
|
||||
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data())).invoke(Unchecked.consumer(out -> {
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
||||
)).invoke(Unchecked.consumer(out -> {
|
||||
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
|
||||
}));
|
||||
else
|
||||
@ -108,6 +113,31 @@ public class CombEndpoints {
|
||||
}
|
||||
|
||||
@POST
|
||||
@RolesAllowed({"federation_admin"})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Uni<Long> addAdminMembre(FullMemberForm input) {
|
||||
return membreService.add(input)
|
||||
.invoke(Unchecked.consumer(id -> {
|
||||
if (id == null) throw new InternalError("Fail to creat member data");
|
||||
})).call(id -> {
|
||||
if (input.getPhoto_data().length > 0)
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
||||
));
|
||||
else
|
||||
return Uni.createFrom().nullItem();
|
||||
});
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@RolesAllowed({"federation_admin"})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public Uni<String> deleteAdminMembre(@PathParam("id") long id) {
|
||||
return membreService.delete(id);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("club/{id}")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@ -118,7 +148,8 @@ public class CombEndpoints {
|
||||
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
||||
})).chain(() -> {
|
||||
if (input.getPhoto_data().length > 0)
|
||||
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data())).invoke(Unchecked.consumer(out -> {
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
||||
)).invoke(Unchecked.consumer(out -> {
|
||||
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
|
||||
}));
|
||||
else
|
||||
@ -126,6 +157,32 @@ public class CombEndpoints {
|
||||
});
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("club")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Uni<Long> addMembre(FullMemberForm input) {
|
||||
return membreService.add(input, idToken.getSubject())
|
||||
.invoke(Unchecked.consumer(id -> {
|
||||
if (id == null) throw new InternalError("Fail to creat member data");
|
||||
})).call(id -> {
|
||||
if (input.getPhoto_data().length > 0)
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
||||
));
|
||||
else
|
||||
return Uni.createFrom().nullItem();
|
||||
});
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("club/{id}")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public Uni<String> deleteMembre(@PathParam("id") long id) {
|
||||
return membreService.delete(id, idToken);
|
||||
}
|
||||
|
||||
private Future<String> replacePhoto(long id, byte[] input) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.rest.from;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
||||
import jakarta.ws.rs.FormParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.Getter;
|
||||
@ -28,4 +29,59 @@ public class AffiliationRequestForm {
|
||||
@FormParam("logo")
|
||||
@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("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("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;
|
||||
|
||||
public AffiliationRequestModel toModel() {
|
||||
AffiliationRequestModel model = new AffiliationRequestModel();
|
||||
model.setName(this.getName());
|
||||
model.setSiren(this.getSiren());
|
||||
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.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.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()));
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
import jodd.net.MimeTypes;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class Utils {
|
||||
|
||||
@ -19,4 +26,35 @@ public class Utils {
|
||||
return calendar.get(Calendar.YEAR) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static Future<String> 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[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
||||
if (detectedExtensions.length == 0)
|
||||
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
|
||||
|
||||
File dirFile = new File(media, dir);
|
||||
if (!dirFile.exists())
|
||||
if (dirFile.mkdirs())
|
||||
throw new IOException("Fail to create directory " + dir);
|
||||
|
||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
|
||||
File[] files = dirFile.listFiles(filter);
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
String extension = "." + detectedExtensions[0];
|
||||
Files.write(new File(dirFile, id + extension).toPath(), input);
|
||||
return "OK";
|
||||
} catch (IOException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
84
src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java
Normal file
84
src/main/java/fr/titionfire/ffsaf/ws/FileSocket.java
Normal file
@ -0,0 +1,84 @@
|
||||
package fr.titionfire.ffsaf.ws;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.websocket.*;
|
||||
import jakarta.websocket.server.PathParam;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@ServerEndpoint("/api/ws/file/{id}")
|
||||
@ApplicationScoped
|
||||
public class FileSocket {
|
||||
Map<String, FileRecv> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session, @PathParam("id") String id) {
|
||||
try {
|
||||
File file = File.createTempFile("safca-", ".tmp");
|
||||
FileRecv fileRecv = new FileRecv(file, new FileOutputStream(file, true), System.currentTimeMillis(),
|
||||
session);
|
||||
System.out.println("File created: " + file.getAbsolutePath());
|
||||
sessions.put(id, fileRecv);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session, @PathParam("id") String id) {
|
||||
if (sessions.containsKey(id)) {
|
||||
FileRecv fileRecv = sessions.get(id);
|
||||
if (fileRecv.fos != null) {
|
||||
try {
|
||||
fileRecv.fos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
sessions.remove(id);
|
||||
}
|
||||
|
||||
@OnError
|
||||
public void onError(Session session, @PathParam("id") String id, Throwable throwable) {
|
||||
if (sessions.containsKey(id)) {
|
||||
FileRecv fileRecv = sessions.get(id);
|
||||
if (fileRecv.fos != null) {
|
||||
try {
|
||||
fileRecv.fos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
sessions.remove(id);
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(String message, @PathParam("id") String id) {
|
||||
if (sessions.containsKey(id)) {
|
||||
FileRecv fileRecv = sessions.get(id);
|
||||
try {
|
||||
fileRecv.fos.write(message.getBytes());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
class FileRecv {
|
||||
File file;
|
||||
FileOutputStream fos;
|
||||
long time;
|
||||
Session session;
|
||||
}
|
||||
}
|
||||
19
src/main/webapp/src/components/ConfirmDialog.jsx
Normal file
19
src/main/webapp/src/components/ConfirmDialog.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) {
|
||||
return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
{title && <h4 className="modal-title" id="myModalLabel">{title}</h4>}
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{message}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>Annuler</button>
|
||||
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>Confirmer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -31,28 +31,35 @@ export function DemandeAff() {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.target)
|
||||
toast.promise(
|
||||
apiAxios.post(`asso/affiliation`, formData),
|
||||
apiAxios.post(`asso/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")
|
||||
})
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>Demande d'affiliation</h1>
|
||||
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année suivante.</p>
|
||||
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année
|
||||
suivante.</p>
|
||||
Pour s’affilier, une association sportive doit réunir les conditions suivantes :
|
||||
<ul>
|
||||
<li>Avoir son siège social en France ou Principauté de Monaco</li>
|
||||
<li>Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport</li>
|
||||
<li>Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération</li>
|
||||
<li>Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la Fédération</li>
|
||||
<li>Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute discrimination</li>
|
||||
<li>Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la Fédération</li>
|
||||
<li>Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la
|
||||
Fédération
|
||||
</li>
|
||||
<li>Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute
|
||||
discrimination
|
||||
</li>
|
||||
<li>Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la
|
||||
Fédération
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="card mb-4">
|
||||
@ -68,16 +75,20 @@ export function DemandeAff() {
|
||||
<MembreInfo role="secretaire"/>
|
||||
|
||||
<div className="mb-3">
|
||||
<p>Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF</p>
|
||||
<p>Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour
|
||||
accéder à votre espace FFSAF</p>
|
||||
Notez que pour finaliser votre affiliation, il vous faudra :
|
||||
<ul>
|
||||
<li>Disposer d’au moins trois membres licenciés, dont le président, le trésorier et le secrétaire</li>
|
||||
<li>Disposer d’au moins trois membres licenciés, dont le président, le trésorier et le
|
||||
secrétaire
|
||||
</li>
|
||||
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button type="submit" className="btn btn-primary">Confirmer ma demande d'affiliation</button>
|
||||
<button type="submit" className="btn btn-primary">Confirmer ma demande d'affiliation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,7 +124,8 @@ function AssoInfo() {
|
||||
return <>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span>
|
||||
<input type="text" className="form-control" placeholder="Nom de l'association" name="name" aria-label="Nom de l'association"
|
||||
<input type="text" className="form-control" placeholder="Nom de l'association" name="name"
|
||||
aria-label="Nom de l'association"
|
||||
aria-describedby="basic-addon1" required/>
|
||||
</div>
|
||||
|
||||
@ -121,24 +133,29 @@ function AssoInfo() {
|
||||
<span className="input-group-text">N° SIREN*</span>
|
||||
<input type="number" className="form-control" placeholder="siren" name="siren" required value={siren}
|
||||
onChange={e => setSiren(e.target.value)}/>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2" onClick={fetchSiren}>Rechercher</button>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
|
||||
onClick={fetchSiren}>Rechercher
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="basic-addon1">Dénomination</span>
|
||||
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter" aria-label="Dénomination"
|
||||
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter"
|
||||
aria-label="Dénomination"
|
||||
aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="basic-addon1">RNA</span>
|
||||
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA" aria-describedby="basic-addon1"
|
||||
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA"
|
||||
aria-describedby="basic-addon1"
|
||||
disabled={!rnaEnable} name="rna" value={rna} onChange={e => setRna(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="basic-addon1">Adresse*</span>
|
||||
<input type="text" className="form-control" placeholder="Adresse" aria-label="Adresse" aria-describedby="basic-addon1"
|
||||
<input type="text" className="form-control" placeholder="Adresse" aria-label="Adresse"
|
||||
aria-describedby="basic-addon1"
|
||||
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
@ -149,7 +166,8 @@ function AssoInfo() {
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="logo">Logo*</label>
|
||||
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg" required/>
|
||||
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg"
|
||||
required/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
@ -159,19 +177,29 @@ function MembreInfo({role}) {
|
||||
<div className="col-sm-3">
|
||||
<div className="form-floating">
|
||||
<input type="text" className="form-control" id="floatingInput" placeholder="Nom" name={role + "-nom"}/>
|
||||
<label htmlFor="floatingInput">Nom*</label>
|
||||
<label htmlFor="floatingInput">Nom</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<div className="form-floating">
|
||||
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom" name={role + "-prenom"}/>
|
||||
<label htmlFor="floatingInput">Prénom*</label>
|
||||
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom"
|
||||
name={role + "-prenom"}/>
|
||||
<label htmlFor="floatingInput">Prénom</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-5">
|
||||
<div className="form-floating">
|
||||
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com" name={role + "-mail"}/>
|
||||
<label htmlFor="floatingInput">Email*</label>
|
||||
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com"
|
||||
name={role + "-mail"}/>
|
||||
<label htmlFor="floatingInput">Email</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<div>OU</div>
|
||||
<div className="form-floating">
|
||||
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
|
||||
name={role + "-licence"}/>
|
||||
<label htmlFor="floatingInput">N° Licence</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -181,7 +209,8 @@ export function DemandeAffOk() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-green-800 text-4xl">Demande d'affiliation envoyée avec succès</h1>
|
||||
<p>Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF</p>
|
||||
<p>Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre
|
||||
espace FFSAF</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@ import './AdminRoot.css'
|
||||
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||
import {MemberList} from "../MemberList.jsx";
|
||||
import {MemberPage} from "./member/MemberPage.jsx";
|
||||
import {NewMemberPage} from "./member/NewMemberPage.jsx";
|
||||
|
||||
export function AdminRoot() {
|
||||
return <>
|
||||
@ -23,6 +24,10 @@ export function getAdminChildren () {
|
||||
path: 'member/:id',
|
||||
element: <MemberPage/>
|
||||
},
|
||||
{
|
||||
path: 'member/new',
|
||||
element: <NewMemberPage/>
|
||||
},
|
||||
{
|
||||
path: 'b',
|
||||
element: <div>Admin B</div>
|
||||
|
||||
@ -5,6 +5,27 @@ import imageCompression from "browser-image-compression";
|
||||
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
|
||||
export function addPhoto(event, formData, send) {
|
||||
const imageFile = event.target.url_photo.files[0];
|
||||
if (imageFile) {
|
||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||
|
||||
const options = {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
}
|
||||
|
||||
imageCompression(imageFile, options).then(compressedFile => {
|
||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||
formData.append("photo_data", compressedFile)
|
||||
send(formData)
|
||||
});
|
||||
} else {
|
||||
send(formData)
|
||||
}
|
||||
}
|
||||
|
||||
export function InformationForm({data}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const handleSubmit = (event) => {
|
||||
@ -25,7 +46,7 @@ export function InformationForm({data}) {
|
||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
apiAxios.post(`/member/${data.id}`, formData_, {
|
||||
apiAxios.put(`/member/${data.id}`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
@ -40,25 +61,7 @@ export function InformationForm({data}) {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
const imageFile = event.target.url_photo.files[0];
|
||||
if (imageFile) {
|
||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||
|
||||
const options = {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
}
|
||||
|
||||
imageCompression(imageFile, options).then(compressedFile => {
|
||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||
formData.append("photo_data", compressedFile)
|
||||
send(formData)
|
||||
});
|
||||
} else {
|
||||
send(formData)
|
||||
}
|
||||
addPhoto(event, formData, send);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
|
||||
@ -6,6 +6,9 @@ import {CompteInfo} from "./CompteInfo.jsx";
|
||||
import {PremForm} from "./PremForm.jsx";
|
||||
import {InformationForm} from "./InformationForm.jsx";
|
||||
import {LicenceCard} from "./LicenceCard.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -16,6 +19,19 @@ export function MemberPage() {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
||||
|
||||
const handleRm = () => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/member/${id}`),
|
||||
{
|
||||
pending: "Suppression du compte en cours...",
|
||||
success: "Compte supprimé avec succès 🎉",
|
||||
error: "Échec de la suppression du compte 😕"
|
||||
}
|
||||
).then(_ => {
|
||||
navigate("/admin/member")
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||
@ -39,6 +55,12 @@ export function MemberPage() {
|
||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
||||
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
||||
onConfirm={handleRm}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
107
src/main/webapp/src/pages/admin/member/NewMemberPage.jsx
Normal file
107
src/main/webapp/src/pages/admin/member/NewMemberPage.jsx
Normal file
@ -0,0 +1,107 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
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 {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
import {addPhoto} from "./InformationForm.jsx";
|
||||
|
||||
export function NewMemberPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||
« retour
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
<Form/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function Form() {
|
||||
const navigate = useNavigate();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", -1);
|
||||
formData.append("lname", event.target.lname?.value);
|
||||
formData.append("fname", event.target.fname?.value);
|
||||
formData.append("categorie", event.target.category?.value);
|
||||
formData.append("club", event.target.club?.value);
|
||||
formData.append("genre", event.target.genre?.value);
|
||||
formData.append("country", event.target.country?.value);
|
||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
||||
formData.append("email", event.target.email?.value);
|
||||
formData.append("role", event.target.role?.value);
|
||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
apiAxios.post(`/member`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
toast.success('Profile crée avec succès 🎉');
|
||||
navigate(`/admin/member/${data.data}`)
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
|
||||
}).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
addPhoto(event, formData, send);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Nouveau membre</div>
|
||||
<div className="card-body">
|
||||
<TextField name="lname" text="Nom"/>
|
||||
<TextField name="fname" text="Prénom"/>
|
||||
<TextField name="email" text="Email" placeholder="name@example.com"
|
||||
type="email"/>
|
||||
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<OptionField name="country" text="Pays" value={'fr'}
|
||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
||||
<BirthDayField/>
|
||||
<div className="row">
|
||||
<ClubSelect name="club"/>
|
||||
</div>
|
||||
<OptionField name="role" text="Rôle" value={'MEMBRE'}
|
||||
values={{
|
||||
MEMBRE: 'Membre',
|
||||
PRESIDENT: 'Président',
|
||||
TRESORIER: 'Trésorier',
|
||||
SECRETAIRE: 'Secrétaire'
|
||||
}}/>
|
||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={'NA'}
|
||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||
(optionnelle)</label>
|
||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Créer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>;
|
||||
}
|
||||
@ -3,6 +3,7 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||
import {MemberPage} from "./member/MemberPage.jsx";
|
||||
import {useAuth} from "../../hooks/useAuth.jsx";
|
||||
import {MemberList} from "../MemberList.jsx";
|
||||
import {NewMemberPage} from "./member/NewMemberPage.jsx";
|
||||
|
||||
export function ClubRoot() {
|
||||
const {userinfo} = useAuth()
|
||||
@ -35,6 +36,10 @@ export function getClubChildren() {
|
||||
path: 'member/:id',
|
||||
element: <MemberPage/>
|
||||
},
|
||||
{
|
||||
path: 'member/new',
|
||||
element: <NewMemberPage/>
|
||||
},
|
||||
{
|
||||
path: 'b',
|
||||
element: <div>Club B</div>
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
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 {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
import {addPhoto} from "../../admin/member/InformationForm.jsx";
|
||||
|
||||
export function InformationForm({data}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
@ -25,7 +24,7 @@ export function InformationForm({data}) {
|
||||
formData.append("role", event.target.role?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
apiAxios.post(`/member/club/${data.id}`, formData_, {
|
||||
apiAxios.put(`/member/club/${data.id}`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
@ -40,23 +39,7 @@ export function InformationForm({data}) {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
const imageFile = event.target.url_photo.files[0];
|
||||
if (imageFile) {
|
||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||
const options = {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
}
|
||||
imageCompression(imageFile, options).then(compressedFile => {
|
||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||
formData.append("photo_data", compressedFile)
|
||||
send(formData)
|
||||
});
|
||||
} else {
|
||||
send(formData)
|
||||
}
|
||||
addPhoto(event, formData, send);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
@ -79,7 +62,7 @@ export function InformationForm({data}) {
|
||||
PRESIDENT: 'Président',
|
||||
TRESORIER: 'Trésorier',
|
||||
SECRETAIRE: 'Secrétaire'
|
||||
}}/>
|
||||
}} disabled={true}/>
|
||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>
|
||||
<div className="row">
|
||||
|
||||
@ -5,6 +5,9 @@ import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {CompteInfo} from "./CompteInfo.jsx";
|
||||
import {InformationForm} from "./InformationForm.jsx";
|
||||
import {LicenceCard} from "./LicenceCard.jsx";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -15,6 +18,19 @@ export function MemberPage() {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
||||
|
||||
const handleRm = () => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/member/club/${id}`),
|
||||
{
|
||||
pending: "Suppression du compte en cours...",
|
||||
success: "Compte supprimé avec succès 🎉",
|
||||
error: "Échec de la suppression du compte 😕"
|
||||
}
|
||||
).then(_ => {
|
||||
navigate("/club/member")
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
||||
@ -37,6 +53,12 @@ export function MemberPage() {
|
||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
||||
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
||||
onConfirm={handleRm}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
92
src/main/webapp/src/pages/club/member/NewMemberPage.jsx
Normal file
92
src/main/webapp/src/pages/club/member/NewMemberPage.jsx
Normal file
@ -0,0 +1,92 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
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 {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
import {addPhoto} from "../../admin/member/InformationForm.jsx";
|
||||
|
||||
export function NewMemberPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
||||
« retour
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
<Form/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function Form() {
|
||||
const navigate = useNavigate();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", -1);
|
||||
formData.append("lname", event.target.lname?.value);
|
||||
formData.append("fname", event.target.fname?.value);
|
||||
formData.append("categorie", event.target.category?.value);
|
||||
formData.append("genre", event.target.genre?.value);
|
||||
formData.append("country", event.target.country?.value);
|
||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
||||
formData.append("email", event.target.email?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
apiAxios.post(`/member/club`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
toast.success('Profile crée avec succès 🎉');
|
||||
navigate(`/club/member/${data.data}`)
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
|
||||
}).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
addPhoto(event, formData, send);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Nouveau membre</div>
|
||||
<div className="card-body">
|
||||
<TextField name="lname" text="Nom"/>
|
||||
<TextField name="fname" text="Prénom"/>
|
||||
<TextField name="email" text="Email" placeholder="name@example.com"
|
||||
type="email"/>
|
||||
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<OptionField name="country" text="Pays" value={'fr'}
|
||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
||||
<BirthDayField/>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||
(optionnelle)</label>
|
||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Créer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user