commit
7d281196c1
@ -0,0 +1,41 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "checkout")
|
||||
public class CheckoutModel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Schema(description = "Identifiant du checkout", example = "42")
|
||||
Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "membre", referencedColumnName = "id")
|
||||
MembreModel membre;
|
||||
|
||||
Date creationDate = new Date();
|
||||
|
||||
List<Long> licenseIds;
|
||||
|
||||
Integer checkoutId;
|
||||
|
||||
PaymentStatus paymentStatus;
|
||||
|
||||
public enum PaymentStatus {
|
||||
PENDING, AUTHORIZED, REFUSED, UNKNOW, REGISTERED, REFUNDING, REFUNDED, CONTESTED
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@Entity
|
||||
@Table(name = "licence")
|
||||
public class LicenceModel {
|
||||
public class LicenceModel implements LoggableModel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Schema(description = "L'identifiant de la licence.")
|
||||
@ -30,9 +30,23 @@ public class LicenceModel {
|
||||
@Schema(description = "La saison de la licence.", example = "2025")
|
||||
int saison;
|
||||
|
||||
@Schema(description = "Nom du médecin sur certificat médical.", example = "M. Jean")
|
||||
@Schema(description = "Nom et date du médecin sur certificat médical.", example = "M. Jean¤2025-02-03", format = "<Nom>¤<yyyy-mm-dd>")
|
||||
String certificate;
|
||||
|
||||
@Schema(description = "Licence validée", example = "true")
|
||||
boolean validate;
|
||||
|
||||
@Schema(description = "Licence payer", example = "true")
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
boolean pay = false;
|
||||
|
||||
@Override
|
||||
public String getObjectName() {
|
||||
return "licence " + id.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogModel.ObjectType getObjectType() {
|
||||
return LogModel.ObjectType.Licence;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package fr.titionfire.ffsaf.data.repository;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CheckoutModel;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CheckoutRepository implements PanacheRepositoryBase<CheckoutModel, Long> {
|
||||
}
|
||||
@ -20,18 +20,11 @@ import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@WithSession
|
||||
@ -62,6 +55,9 @@ public class AffiliationService {
|
||||
@Inject
|
||||
ReactiveMailer reactiveMailer;
|
||||
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
@ -270,7 +266,9 @@ public class AffiliationService {
|
||||
.call(l1 -> l1 != null && l1.stream().anyMatch(l -> l.getSaison() == saison) ?
|
||||
Uni.createFrom().nullItem() :
|
||||
Panache.withTransaction(() -> licenceRepository.persist(
|
||||
new LicenceModel(null, m, club.getId(), saison, null, true)))));
|
||||
new LicenceModel(null, m, club.getId(), saison, null, true, false)))
|
||||
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, m.getObjectName(),
|
||||
licenceModel))));
|
||||
}
|
||||
|
||||
public Uni<?> accept(AffiliationRequestSaveForm form) {
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CheckoutModel;
|
||||
import fr.titionfire.ffsaf.data.model.LogModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CheckoutRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
||||
import fr.titionfire.ffsaf.rest.client.HelloAssoService;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsRequest;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsResponse;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.CheckoutMetadata;
|
||||
import fr.titionfire.ffsaf.rest.exception.DInternalError;
|
||||
import fr.titionfire.ffsaf.utils.SecurityCtx;
|
||||
import fr.titionfire.ffsaf.utils.Utils;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
public class CheckoutService {
|
||||
|
||||
@Inject
|
||||
CheckoutRepository repository;
|
||||
|
||||
@Inject
|
||||
LicenceRepository licenceRepository;
|
||||
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
@Inject
|
||||
LicenceService licenceService;
|
||||
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@RestClient
|
||||
HelloAssoService helloAssoService;
|
||||
|
||||
@ConfigProperty(name = "frontRootUrl")
|
||||
String frontRootUrl;
|
||||
|
||||
@ConfigProperty(name = "unitLicencePrice")
|
||||
int unitLicencePrice;
|
||||
|
||||
@ConfigProperty(name = "helloasso.organizationSlug")
|
||||
String organizationSlug;
|
||||
|
||||
public Uni<Boolean> canDeleteLicence(long id) {
|
||||
return repository.find("?1 IN licenseIds", id).count().map(count -> count == 0);
|
||||
}
|
||||
|
||||
public Uni<String> create(List<Long> ids, SecurityCtx securityCtx) {
|
||||
return membreService.getByAccountId(securityCtx.getSubject())
|
||||
.call(membreModel -> Mutiny.fetch(membreModel.getClub()))
|
||||
.chain(membreModel -> {
|
||||
CheckoutModel model = new CheckoutModel();
|
||||
model.setMembre(membreModel);
|
||||
model.setLicenseIds(ids);
|
||||
model.setPaymentStatus(CheckoutModel.PaymentStatus.UNKNOW);
|
||||
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
})
|
||||
.chain(checkoutModel -> {
|
||||
CheckoutIntentsRequest request = new CheckoutIntentsRequest();
|
||||
request.setTotalAmount(unitLicencePrice * checkoutModel.getLicenseIds().size());
|
||||
request.setInitialAmount(unitLicencePrice * checkoutModel.getLicenseIds().size());
|
||||
request.setItemName("%d licences %d-%d pour %s".formatted(checkoutModel.getLicenseIds().size(),
|
||||
Utils.getSaison(), Utils.getSaison() + 1, checkoutModel.getMembre().getClub().getName()));
|
||||
request.setBackUrl(frontRootUrl + "/club/member/pay");
|
||||
request.setErrorUrl(frontRootUrl + "/club/member/pay/error");
|
||||
request.setReturnUrl(frontRootUrl + "/club/member/pay/return");
|
||||
request.setContainsDonation(false);
|
||||
request.setPayer(new CheckoutIntentsRequest.Payer(checkoutModel.getMembre().getFname(),
|
||||
checkoutModel.getMembre().getLname(), checkoutModel.getMembre().getEmail()));
|
||||
request.setMetadata(new CheckoutMetadata(checkoutModel.getId()));
|
||||
|
||||
return helloAssoService.checkout(organizationSlug, request)
|
||||
.call(response -> {
|
||||
checkoutModel.setCheckoutId(response.getId());
|
||||
return Panache.withTransaction(() -> repository.persist(checkoutModel));
|
||||
});
|
||||
})
|
||||
.onFailure().transform(t -> new DInternalError(t.getMessage()))
|
||||
.map(CheckoutIntentsResponse::getRedirectUrl);
|
||||
}
|
||||
|
||||
public Uni<Response> paymentStatusChange(String state, CheckoutMetadata metadata) {
|
||||
return repository.findById(metadata.getCheckoutDBId())
|
||||
.chain(checkoutModel -> {
|
||||
CheckoutModel.PaymentStatus newStatus = CheckoutModel.PaymentStatus.valueOf(state.toUpperCase());
|
||||
|
||||
Uni<?> uni = Uni.createFrom().nullItem();
|
||||
|
||||
if (checkoutModel.getPaymentStatus().equals(newStatus))
|
||||
return uni;
|
||||
|
||||
if (newStatus.equals(CheckoutModel.PaymentStatus.AUTHORIZED)) {
|
||||
for (Long id : checkoutModel.getLicenseIds()) {
|
||||
uni = uni.chain(__ -> licenceRepository.findById(id)
|
||||
.onFailure().recoverWithNull()
|
||||
.call(licenceModel -> {
|
||||
if (licenceModel == null) {
|
||||
ls.logAnonymous(LogModel.ActionType.UPDATE, LogModel.ObjectType.Licence,
|
||||
"Fail to save payment for licence (checkout n°" + checkoutModel.getCheckoutId() + ")",
|
||||
"", id);
|
||||
return Uni.createFrom().nullItem();
|
||||
}
|
||||
|
||||
ls.logUpdateAnonymous("Paiement de la licence", licenceModel);
|
||||
licenceModel.setPay(true);
|
||||
|
||||
if (licenceModel.getCertificate() != null && licenceModel.getCertificate()
|
||||
.length() > 3) {
|
||||
if (!licenceModel.isValidate())
|
||||
ls.logUpdateAnonymous("Validation automatique de la licence",
|
||||
licenceModel);
|
||||
return licenceService.validateLicences(licenceModel);
|
||||
} else {
|
||||
return Panache.withTransaction(
|
||||
() -> licenceRepository.persist(licenceModel));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else if (checkoutModel.getPaymentStatus().equals(CheckoutModel.PaymentStatus.AUTHORIZED)) {
|
||||
for (Long id : checkoutModel.getLicenseIds()) {
|
||||
uni = uni.chain(__ -> licenceRepository.findById(id)
|
||||
.onFailure().recoverWithNull()
|
||||
.call(licenceModel -> {
|
||||
if (licenceModel == null)
|
||||
return Uni.createFrom().nullItem();
|
||||
|
||||
ls.logUpdateAnonymous("Annulation automatique du paiement de la licence",
|
||||
licenceModel);
|
||||
licenceModel.setPay(false);
|
||||
if (licenceModel.isValidate())
|
||||
ls.logUpdateAnonymous(
|
||||
"Annulation automatique de la validation de la licence",
|
||||
licenceModel);
|
||||
licenceModel.setValidate(false);
|
||||
return Panache.withTransaction(() -> licenceRepository.persist(licenceModel));
|
||||
}));
|
||||
}
|
||||
}
|
||||
uni = uni.call(__ -> ls.append());
|
||||
|
||||
checkoutModel.setPaymentStatus(newStatus);
|
||||
return uni.chain(__ -> Panache.withTransaction(() -> repository.persist(checkoutModel)));
|
||||
})
|
||||
.onFailure().invoke(Throwable::printStackTrace)
|
||||
.map(__ -> Response.ok().build());
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
Uni<Void> everyHours() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.HOUR, -1);
|
||||
Date dateLimit = calendar.getTime();
|
||||
|
||||
return repository.delete("creationDate < ?1 AND (checkoutId IS NULL OR paymentStatus = ?2)", dateLimit,
|
||||
CheckoutModel.PaymentStatus.UNKNOW)
|
||||
.map(__ -> null);
|
||||
}
|
||||
}
|
||||
@ -31,16 +31,10 @@ import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
|
||||
@ -48,7 +42,6 @@ import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
public class ClubService {
|
||||
private static final Logger LOGGER = Logger.getLogger(ClubService.class);
|
||||
|
||||
@Inject
|
||||
ClubRepository repository;
|
||||
@ -68,12 +61,6 @@ public class ClubService {
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@ConfigProperty(name = "pdf-maker.jar-path")
|
||||
String pdfMakerJarPath;
|
||||
|
||||
@ConfigProperty(name = "pdf-maker.sign-file")
|
||||
String sign_file;
|
||||
|
||||
public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
|
||||
return VertxContextSupport.subscribeAndAwait(
|
||||
() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
|
||||
@ -346,120 +333,4 @@ public class ClubService {
|
||||
return data;
|
||||
}).collect().asList();
|
||||
}
|
||||
|
||||
public Uni<Response> getAffiliationPdf(String subject) {
|
||||
return getAffiliationPdf(
|
||||
combRepository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null || m.getClub() == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
}))
|
||||
.map(MembreModel::getClub)
|
||||
.call(m -> Mutiny.fetch(m.getAffiliations())));
|
||||
}
|
||||
|
||||
public Uni<Response> getAffiliationPdf(long id) {
|
||||
return getAffiliationPdf(
|
||||
repository.findById(id)
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
}))
|
||||
.call(m -> Mutiny.fetch(m.getAffiliations())));
|
||||
}
|
||||
|
||||
|
||||
private Uni<Response> getAffiliationPdf(Uni<ClubModel> uniBase) {
|
||||
return uniBase
|
||||
.map(Unchecked.function(m -> {
|
||||
if (m.getAffiliations().stream()
|
||||
.noneMatch(licenceModel -> licenceModel.getSaison() == Utils.getSaison()))
|
||||
throw new DNotFoundException("Pas d'affiliation pour la saison en cours");
|
||||
|
||||
try {
|
||||
byte[] buff = make_pdf(m);
|
||||
if (buff == null)
|
||||
throw new IOException("Error making pdf");
|
||||
|
||||
String mimeType = "application/pdf";
|
||||
|
||||
Response.ResponseBuilder resp = Response.ok(buff);
|
||||
resp.type(MediaType.APPLICATION_OCTET_STREAM);
|
||||
resp.header(HttpHeaders.CONTENT_LENGTH, buff.length);
|
||||
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
|
||||
resp.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"inline; " + "filename=\"Attestation d'affiliation " + Utils.getSaison() + "-" +
|
||||
(Utils.getSaison() + 1) + " de " + m.getName() + ".pdf\"");
|
||||
return resp.build();
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private byte[] make_pdf(ClubModel m) throws IOException, InterruptedException {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add("java");
|
||||
cmd.add("-jar");
|
||||
cmd.add(pdfMakerJarPath);
|
||||
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
cmd.add("/tmp/" + uuid + ".pdf");
|
||||
cmd.add("club");
|
||||
cmd.add(m.getName());
|
||||
cmd.add(Utils.getSaison() + "");
|
||||
cmd.add(m.getNo_affiliation() + "");
|
||||
cmd.add(new File(sign_file).getAbsolutePath());
|
||||
|
||||
return getPdf(cmd, uuid, LOGGER);
|
||||
}
|
||||
|
||||
static byte[] getPdf(List<String> cmd, UUID uuid, Logger logger) throws IOException, InterruptedException {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
|
||||
processBuilder.redirectErrorStream(true);
|
||||
Process process = processBuilder.start();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
builder.append(line).append("\n");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
|
||||
int code = -1;
|
||||
if (!process.waitFor(30, TimeUnit.SECONDS)) {
|
||||
process.destroy();
|
||||
builder.append("Timeout...");
|
||||
} else {
|
||||
code = process.exitValue();
|
||||
}
|
||||
|
||||
if (t.isAlive())
|
||||
t.interrupt();
|
||||
|
||||
logger.debug("PDF maker: " + builder);
|
||||
|
||||
if (code != 0) {
|
||||
throw new IOException("Error code: " + code);
|
||||
} else {
|
||||
File file = new File("/tmp/" + uuid + ".pdf");
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] buff = fis.readAllBytes();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
return buff;
|
||||
} catch (IOException e) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.client.HelloAssoAuthClient;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.TokenResponse;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
@ApplicationScoped
|
||||
public class HelloAssoTokenService {
|
||||
private static final Logger LOG = Logger.getLogger(HelloAssoTokenService.class);
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
HelloAssoAuthClient authClient;
|
||||
|
||||
@ConfigProperty(name = "helloasso.client-id")
|
||||
String clientId;
|
||||
|
||||
@ConfigProperty(name = "helloasso.client-secret")
|
||||
String clientSecret;
|
||||
|
||||
private TokenResponse currentToken; // Stockage en mémoire (pour un seul pod)
|
||||
|
||||
// Récupère un token valide (en le rafraîchissant si nécessaire)
|
||||
public Uni<String> getValidAccessToken() {
|
||||
if (currentToken == null || currentToken.isExpired()) {
|
||||
return fetchNewToken(clientId, clientSecret);
|
||||
}
|
||||
return Uni.createFrom().item(currentToken.accessToken);
|
||||
}
|
||||
|
||||
// Récupère un nouveau token (via client_credentials ou refresh_token)
|
||||
private Uni<String> fetchNewToken(String clientId, String clientSecret) {
|
||||
if (currentToken != null && currentToken.refreshToken != null) {
|
||||
// On utilise le refresh_token si disponible
|
||||
return authClient.refreshToken("refresh_token", clientId, currentToken.refreshToken)
|
||||
.onItem().invoke(token -> {
|
||||
LOG.info("Token rafraîchi avec succès");
|
||||
currentToken = token;
|
||||
})
|
||||
.onFailure().recoverWithItem(e -> {
|
||||
LOG.warn("Échec du rafraîchissement, utilisation des credentials", e);
|
||||
return null; // Force l'utilisation des credentials
|
||||
})
|
||||
.flatMap(token -> token != null ?
|
||||
Uni.createFrom().item(token.accessToken) :
|
||||
getTokenWithCredentials(clientId, clientSecret)
|
||||
);
|
||||
} else {
|
||||
return getTokenWithCredentials(clientId, clientSecret);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupère un token avec client_id/client_secret
|
||||
private Uni<String> getTokenWithCredentials(String clientId, String clientSecret) {
|
||||
return authClient.getToken("client_credentials", clientId, clientSecret)
|
||||
.onItem().invoke(token -> {
|
||||
LOG.info("Nouveau token obtenu");
|
||||
currentToken = token;
|
||||
})
|
||||
.onFailure().invoke(e -> LOG.error("Erreur lors de l'obtention du token", e))
|
||||
.map(token -> token.accessToken);
|
||||
}
|
||||
}
|
||||
@ -260,7 +260,7 @@ public class KeycloakService {
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Suite à votre première inscription à la Fédération Française de Soft Armored Fighting (FFSAF), votre compte pour accéder à l'intranet a été créé.
|
||||
Suite à votre première inscription %sà la Fédération Française de Soft Armored Fighting (FFSAF), votre compte pour accéder à l'intranet a été créé.
|
||||
Ce compte vous permettra de consulter vos informations, de vous inscrire aux compétitions et de consulter vos résultats.
|
||||
|
||||
Vous allez recevoir dans les prochaines minutes un email vous demandant de vérifier votre email et de définir un mot de passe.
|
||||
@ -273,7 +273,9 @@ public class KeycloakService {
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", user.getUsername())
|
||||
""",
|
||||
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
|
||||
.getName() + ") " : "", user.getUsername())
|
||||
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
|
||||
) : Uni.createFrom().nullItem())
|
||||
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||
import fr.titionfire.ffsaf.data.model.LogModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
||||
@ -20,6 +21,7 @@ import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@ -34,6 +36,15 @@ public class LicenceService {
|
||||
@Inject
|
||||
SequenceRepository sequenceRepository;
|
||||
|
||||
@Inject
|
||||
KeycloakService keycloakService;
|
||||
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@Inject
|
||||
CheckoutService checkoutService;
|
||||
|
||||
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
|
||||
return combRepository.findById(id).invoke(checkPerm)
|
||||
.chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
||||
@ -48,6 +59,29 @@ public class LicenceService {
|
||||
.chain(membres -> repository.find("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres).list());
|
||||
}
|
||||
|
||||
public Uni<?> valideLicences(List<Long> ids) {
|
||||
Uni<String> uni = Uni.createFrom().nullItem();
|
||||
|
||||
for (Long id : ids) {
|
||||
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
|
||||
.chain(model -> {
|
||||
if (!model.isValidate())
|
||||
ls.logUpdate("validation de la licence", model);
|
||||
return validateLicences(model);
|
||||
}))
|
||||
.map(__ -> "OK");
|
||||
}
|
||||
return uni.call(__ -> ls.append());
|
||||
}
|
||||
|
||||
protected Uni<LicenceModel> validateLicences(LicenceModel model) {
|
||||
model.setValidate(true);
|
||||
return Panache.withTransaction(() -> repository.persist(model)
|
||||
.call(m -> Mutiny.fetch(m.getMembre())
|
||||
.call(genLicenceNumberAndAccountIfNeed())
|
||||
));
|
||||
}
|
||||
|
||||
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
||||
if (form.getId() == -1) {
|
||||
return combRepository.findById(id).chain(membreModel -> {
|
||||
@ -57,33 +91,69 @@ public class LicenceService {
|
||||
model.setSaison(form.getSaison());
|
||||
model.setCertificate(form.getCertificate());
|
||||
model.setValidate(form.isValidate());
|
||||
model.setPay(form.isPay());
|
||||
return Panache.withTransaction(() -> repository.persist(model)
|
||||
.call(m -> (m.isValidate() && membreModel.getLicence() <= 0) ?
|
||||
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
||||
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
||||
.chain(() -> combRepository.persist(membreModel))
|
||||
: Uni.createFrom().nullItem()
|
||||
));
|
||||
.call(m -> m.isValidate() ? Uni.createFrom().item(membreModel)
|
||||
.call(genLicenceNumberAndAccountIfNeed())
|
||||
: Uni.createFrom().nullItem()
|
||||
))
|
||||
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
|
||||
licenceModel));
|
||||
});
|
||||
} else {
|
||||
return repository.findById(form.getId()).chain(model -> {
|
||||
ls.logChange("Certificate", model.getCertificate(), form.getCertificate(), model);
|
||||
ls.logChange("Validate", model.isValidate(), form.isValidate(), model);
|
||||
ls.logChange("Pay", model.isPay(), form.isPay(), model);
|
||||
model.setCertificate(form.getCertificate());
|
||||
model.setValidate(form.isValidate());
|
||||
model.setPay(form.isPay());
|
||||
return Panache.withTransaction(() -> repository.persist(model)
|
||||
.call(m -> m.isValidate() ? Mutiny.fetch(m.getMembre())
|
||||
.call(membreModel -> (membreModel.getLicence() <= 0) ?
|
||||
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
||||
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
||||
.chain(() -> combRepository.persist(membreModel))
|
||||
: Uni.createFrom().nullItem())
|
||||
: Uni.createFrom().nullItem()
|
||||
));
|
||||
.call(m -> m.isValidate() ? Mutiny.fetch(m.getMembre())
|
||||
.call(genLicenceNumberAndAccountIfNeed())
|
||||
: Uni.createFrom().nullItem()
|
||||
))
|
||||
.call(__ -> ls.append());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() {
|
||||
return membreModel -> ((membreModel.getLicence() <= 0) ?
|
||||
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
||||
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
||||
.chain(() -> combRepository.persist(membreModel))
|
||||
: Uni.createFrom().nullItem())
|
||||
.call(__ -> (membreModel.getUserId() == null) ?
|
||||
keycloakService.initCompte(membreModel.getId())
|
||||
: Uni.createFrom().nullItem());
|
||||
}
|
||||
|
||||
public Uni<String> payLicences(List<Long> ids, Consumer<MembreModel> checkPerm, SecurityCtx securityCtx) {
|
||||
return repository.list("membre.id IN ?1 AND saison = ?2 AND pay = FALSE", ids, Utils.getSaison())
|
||||
.invoke(Unchecked.consumer(models -> {
|
||||
if (models.size() != ids.size())
|
||||
throw new DBadRequestException("Erreur lors de la sélection des membres");
|
||||
}))
|
||||
.call(models -> {
|
||||
Uni<?> uni = Uni.createFrom().nullItem();
|
||||
for (LicenceModel model : models)
|
||||
uni = uni.chain(__ -> Mutiny.fetch(model.getMembre()).invoke(checkPerm));
|
||||
return uni;
|
||||
})
|
||||
.chain(models -> checkoutService.create(models.stream().map(LicenceModel::getId).toList(),
|
||||
securityCtx));
|
||||
}
|
||||
|
||||
public Uni<?> deleteLicence(long id) {
|
||||
return Panache.withTransaction(() -> repository.deleteById(id));
|
||||
return repository.findById(id)
|
||||
.call(__ -> checkoutService.canDeleteLicence(id)
|
||||
.invoke(Unchecked.consumer(b -> {
|
||||
if (!b) throw new DBadRequestException(
|
||||
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
|
||||
})))
|
||||
.call(model -> ls.logADelete(model))
|
||||
.chain(model -> repository.delete(model));
|
||||
}
|
||||
|
||||
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) {
|
||||
@ -101,11 +171,15 @@ public class LicenceService {
|
||||
model.setCertificate(form.getCertificate());
|
||||
model.setValidate(false);
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
}));
|
||||
}))
|
||||
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
|
||||
licenceModel));
|
||||
} else {
|
||||
return repository.findById(form.getId()).chain(model -> {
|
||||
ls.logChange("Certificate", model.getCertificate(), form.getCertificate(), model);
|
||||
model.setCertificate(form.getCertificate());
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
return Panache.withTransaction(() -> repository.persist(model))
|
||||
.call(__ -> ls.append());
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -114,10 +188,18 @@ public class LicenceService {
|
||||
public Uni<?> deleteAskLicence(long id, Consumer<MembreModel> checkPerm) {
|
||||
return repository.findById(id)
|
||||
.call(licenceModel -> Mutiny.fetch(licenceModel.getMembre()).invoke(checkPerm))
|
||||
.call(__ -> checkoutService.canDeleteLicence(id)
|
||||
.invoke(Unchecked.consumer(b -> {
|
||||
if (!b) throw new DBadRequestException(
|
||||
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
|
||||
})))
|
||||
.invoke(Unchecked.consumer(licenceModel -> {
|
||||
if (licenceModel.isValidate())
|
||||
throw new DBadRequestException("Impossible de supprimer une licence déjà validée");
|
||||
if (licenceModel.isPay())
|
||||
throw new DBadRequestException("Impossible de supprimer une licence déjà payée");
|
||||
}))
|
||||
.call(model -> ls.logADelete(model))
|
||||
.chain(__ -> Panache.withTransaction(() -> repository.deleteById(id)));
|
||||
}
|
||||
|
||||
|
||||
@ -71,10 +71,18 @@ public class LoggerService {
|
||||
message));
|
||||
}
|
||||
|
||||
public void logAnonymous(ActionType action, ObjectType object, String message, String target_name, Long target_id) {
|
||||
buffer.add(new LogModel(null, null, new Date(), action, object, target_id, target_name, message));
|
||||
}
|
||||
|
||||
public void log(ActionType action, String message, LoggableModel model) {
|
||||
log(action, model.getObjectType(), message, model.getObjectName(), model.getId());
|
||||
}
|
||||
|
||||
public void logAnonymous(ActionType action, String message, LoggableModel model) {
|
||||
logAnonymous(action, model.getObjectType(), message, model.getObjectName(), model.getId());
|
||||
}
|
||||
|
||||
public void logAdd(LoggableModel model) {
|
||||
log(ActionType.ADD, "", model);
|
||||
}
|
||||
@ -83,6 +91,10 @@ public class LoggerService {
|
||||
log(ActionType.UPDATE, message, model);
|
||||
}
|
||||
|
||||
public void logUpdateAnonymous(String message, LoggableModel model) {
|
||||
logAnonymous(ActionType.UPDATE, message, model);
|
||||
}
|
||||
|
||||
public void logChange(String champ, Object o1, Object o2, LoggableModel model) {
|
||||
if (Objects.equals(o1, o2))
|
||||
return;
|
||||
|
||||
@ -13,7 +13,6 @@ import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData;
|
||||
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
|
||||
import fr.titionfire.ffsaf.utils.*;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
@ -27,22 +26,13 @@ import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static fr.titionfire.ffsaf.domain.service.ClubService.getPdf;
|
||||
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@ -67,8 +57,6 @@ public class MembreService {
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
@ConfigProperty(name = "pdf-maker.jar-path")
|
||||
String pdfMakerJarPath;
|
||||
@Inject
|
||||
RegisterRepository registerRepository;
|
||||
|
||||
@ -90,46 +78,93 @@ public class MembreService {
|
||||
final static String FIND_NAME_REQUEST = "unaccent(fname) ILIKE unaccent(?1) OR unaccent(lname) ILIKE unaccent(?1) " +
|
||||
"OR unaccent(fname || ' ' || lname) ILIKE unaccent(?1) OR unaccent(lname || ' ' || fname) ILIKE unaccent(?1)";
|
||||
|
||||
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club) {
|
||||
if (search == null)
|
||||
search = "";
|
||||
search = "%" + search.replaceAll(" ", "% %") + "%";
|
||||
|
||||
PanacheQuery<MembreModel> query;
|
||||
|
||||
if (club == null || club.isBlank()) {
|
||||
query = repository.find(FIND_NAME_REQUEST, Sort.ascending("fname", "lname"), search)
|
||||
.page(Page.ofSize(limit));
|
||||
} else {
|
||||
if (club.equals("null")) {
|
||||
query = repository.find(
|
||||
"club IS NULL AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), search).page(Page.ofSize(limit));
|
||||
} else {
|
||||
query = repository.find(
|
||||
"LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), search, club + "%").page(Page.ofSize(limit));
|
||||
}
|
||||
}
|
||||
return getPageResult(query, limit, page);
|
||||
private Uni<List<LicenceModel>> getLicenceListe(int licenceRequest, int payState) {
|
||||
Uni<List<LicenceModel>> baseUni;
|
||||
String queryStr = "saison = ?1";
|
||||
if (payState == 0)
|
||||
queryStr += " AND pay = FALSE";
|
||||
if (payState == 1)
|
||||
queryStr += " AND pay = TRUE";
|
||||
if (licenceRequest == 0 || licenceRequest == 1)
|
||||
baseUni = licenceRepository.list(queryStr, Utils.getSaison());
|
||||
else if (licenceRequest == 2)
|
||||
baseUni = licenceRepository.list(queryStr + " AND validate = FALSE", Utils.getSaison());
|
||||
else if (licenceRequest == 5)
|
||||
baseUni = licenceRepository.list(queryStr + " AND validate = FALSE AND LENGTH(certificate) >= 3",
|
||||
Utils.getSaison());
|
||||
else if (licenceRequest == 6)
|
||||
baseUni = licenceRepository.list(queryStr + " AND validate = FALSE AND LENGTH(certificate) <= 2",
|
||||
Utils.getSaison());
|
||||
else if (licenceRequest == 3)
|
||||
baseUni = licenceRepository.list(queryStr + " AND validate = TRUE", Utils.getSaison());
|
||||
else
|
||||
baseUni = Uni.createFrom().item(new ArrayList<>());
|
||||
return baseUni;
|
||||
}
|
||||
|
||||
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, String subject) {
|
||||
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club,
|
||||
int licenceRequest, int payState) {
|
||||
if (search == null)
|
||||
search = "";
|
||||
search = "%" + search.replaceAll(" ", "% %") + "%";
|
||||
|
||||
String finalSearch = search;
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.chain(membreModel -> {
|
||||
PanacheQuery<MembreModel> query = repository.find(
|
||||
"club = ?2 AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub())
|
||||
.page(Page.ofSize(limit));
|
||||
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
|
||||
|
||||
return baseUni
|
||||
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
|
||||
.chain(ids -> {
|
||||
PanacheQuery<MembreModel> query;
|
||||
|
||||
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
|
||||
|
||||
if (club == null || club.isBlank()) {
|
||||
query = repository.find(
|
||||
"id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), finalSearch, ids)
|
||||
.page(Page.ofSize(limit));
|
||||
} else {
|
||||
if (club.equals("null")) {
|
||||
query = repository.find(
|
||||
"id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), finalSearch, ids).page(Page.ofSize(limit));
|
||||
} else {
|
||||
query = repository.find(
|
||||
"id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), finalSearch, club + "%", ids)
|
||||
.page(Page.ofSize(limit));
|
||||
}
|
||||
}
|
||||
return getPageResult(query, limit, page);
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
|
||||
String subject) {
|
||||
if (search == null)
|
||||
search = "";
|
||||
search = "%" + search.replaceAll(" ", "% %") + "%";
|
||||
|
||||
String finalSearch = search;
|
||||
|
||||
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
|
||||
|
||||
return baseUni
|
||||
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
|
||||
.chain(ids -> {
|
||||
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
|
||||
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.chain(membreModel -> {
|
||||
PanacheQuery<MembreModel> query = repository.find(
|
||||
"id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")",
|
||||
Sort.ascending("fname", "lname"), finalSearch, membreModel.getClub(), ids)
|
||||
.page(Page.ofSize(limit));
|
||||
return getPageResult(query, limit, page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Uni<PageResult<SimpleMembre>> getPageResult(PanacheQuery<MembreModel> query, int limit, int page) {
|
||||
return Uni.createFrom().item(new PageResult<SimpleMembre>())
|
||||
.invoke(result -> result.setPage(page))
|
||||
@ -167,9 +202,10 @@ public class MembreService {
|
||||
clubModel.set(membreModel.getClub());
|
||||
if (data2.stream().noneMatch(d -> d.getLicence() != null))
|
||||
return Uni.createFrom().item(new ArrayList<MembreModel>());
|
||||
return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2",
|
||||
return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3",
|
||||
data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(),
|
||||
data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList());
|
||||
data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(),
|
||||
data2.stream().map(SimpleMembreInOutData::getEmail).filter(Objects::nonNull).toList());
|
||||
})
|
||||
.call(Unchecked.function(membres -> {
|
||||
for (MembreModel membreModel : membres) {
|
||||
@ -181,7 +217,8 @@ public class MembreService {
|
||||
for (SimpleMembreInOutData dataIn : data2) {
|
||||
MembreModel model = membres.stream()
|
||||
.filter(m -> Objects.equals(m.getLicence(), dataIn.getLicence()) || m.getLname()
|
||||
.equals(dataIn.getNom()) && m.getFname().equals(dataIn.getPrenom())).findFirst()
|
||||
.equals(dataIn.getNom()) && m.getFname().equals(dataIn.getPrenom()) ||
|
||||
Objects.equals(m.getFname(), dataIn.getEmail())).findFirst()
|
||||
.orElseGet(() -> {
|
||||
MembreModel mm = new MembreModel();
|
||||
mm.setClub(clubModel.get());
|
||||
@ -190,6 +227,18 @@ public class MembreService {
|
||||
return mm;
|
||||
});
|
||||
|
||||
if (model.getEmail() != null) {
|
||||
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
|
||||
throw new DBadRequestException("Email déja utiliser");
|
||||
}
|
||||
|
||||
if (StringSimilarity.similarity(model.getLname().toUpperCase(),
|
||||
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
|
||||
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
|
||||
throw new DBadRequestException("Email déja utiliser");
|
||||
}
|
||||
}
|
||||
|
||||
boolean add = model.getId() == null;
|
||||
|
||||
if ((!add && StringSimilarity.similarity(model.getLname().toUpperCase(),
|
||||
@ -268,6 +317,11 @@ public class MembreService {
|
||||
|
||||
public Uni<String> update(long id, FullMemberForm membre) {
|
||||
return update(repository.findById(id)
|
||||
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0)
|
||||
throw new DBadRequestException("Email déjà utiliser");
|
||||
})))
|
||||
.chain(membreModel -> clubRepository.findById(membre.getClub())
|
||||
.map(club -> new Pair<>(membreModel, club)))
|
||||
.onItem().transform(pair -> {
|
||||
@ -285,9 +339,20 @@ public class MembreService {
|
||||
|
||||
public Uni<String> update(long id, FullMemberForm membre, SecurityCtx securityCtx) {
|
||||
return update(repository.findById(id)
|
||||
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0)
|
||||
throw new DBadRequestException("Email déjà utiliser");
|
||||
})))
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
|
||||
throw new DForbiddenException();
|
||||
if (StringSimilarity.similarity(membreModel.getLname().toUpperCase(),
|
||||
membre.getLname().toUpperCase()) > 3 || StringSimilarity.similarity(
|
||||
membreModel.getFname().toUpperCase(), membre.getFname().toUpperCase()) > 3) {
|
||||
throw new DBadRequestException(
|
||||
"Pour enregistrer un nouveau membre, veuillez utilisez le bouton prévue a cette effet.");
|
||||
}
|
||||
}))
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
RoleAsso source = RoleAsso.MEMBRE;
|
||||
@ -365,6 +430,10 @@ public class MembreService {
|
||||
|
||||
public Uni<Long> add(FullMemberForm input) {
|
||||
return clubRepository.findById(input.getClub())
|
||||
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0) throw new DBadRequestException("Email déjà utiliser");
|
||||
})))
|
||||
.chain(clubModel -> {
|
||||
MembreModel model = getMembreModel(input, clubModel);
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
@ -377,6 +446,18 @@ public class MembreService {
|
||||
|
||||
public Uni<Long> add(FullMemberForm input, String subject) {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0) throw new DBadRequestException("Email déjà utiliser");
|
||||
})))
|
||||
.call(membreModel ->
|
||||
repository.count(
|
||||
"unaccent(lname) ILIKE unaccent(?2) AND unaccent(fname) ILIKE unaccent(?2) AND club = ?3",
|
||||
input.getLname(), input.getFname(), membreModel.getClub())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0)
|
||||
throw new DBadRequestException("Membre déjà existent");
|
||||
})))
|
||||
.chain(membreModel -> {
|
||||
MembreModel model = getMembreModel(input, membreModel.getClub());
|
||||
model.setRole(RoleAsso.MEMBRE);
|
||||
@ -477,70 +558,4 @@ public class MembreService {
|
||||
.map(__ -> null);
|
||||
}
|
||||
|
||||
public Uni<Response> getLicencePdf(String subject) {
|
||||
return getLicencePdf(repository.find("userId = ?1", subject).firstResult()
|
||||
.call(m -> Mutiny.fetch(m.getLicences())));
|
||||
}
|
||||
|
||||
public Uni<Response> getLicencePdf(Uni<MembreModel> uniBase) {
|
||||
return uniBase
|
||||
.map(Unchecked.function(m -> {
|
||||
LicenceModel licence = m.getLicences().stream()
|
||||
.filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DNotFoundException("Pas de licence pour la saison en cours"));
|
||||
|
||||
try {
|
||||
byte[] buff = make_pdf(m, licence);
|
||||
if (buff == null)
|
||||
throw new IOException("Error making pdf");
|
||||
|
||||
String mimeType = "application/pdf";
|
||||
|
||||
Response.ResponseBuilder resp = Response.ok(buff);
|
||||
resp.type(MediaType.APPLICATION_OCTET_STREAM);
|
||||
resp.header(HttpHeaders.CONTENT_LENGTH, buff.length);
|
||||
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
|
||||
resp.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"inline; " + "filename=\"Attestation d'adhésion " + Utils.getSaison() + "-" +
|
||||
(Utils.getSaison() + 1) + " de " + m.getLname() + " " + m.getFname() + ".pdf\"");
|
||||
return resp.build();
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private byte[] make_pdf(MembreModel m, LicenceModel licence) throws IOException, InterruptedException {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add("java");
|
||||
cmd.add("-jar");
|
||||
cmd.add(pdfMakerJarPath);
|
||||
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
cmd.add("/tmp/" + uuid + ".pdf");
|
||||
cmd.add("membre");
|
||||
cmd.add(m.getFname());
|
||||
cmd.add(m.getLname());
|
||||
cmd.add(m.getGenre().str);
|
||||
cmd.add(m.getCategorie().getName());
|
||||
cmd.add(licence.getCertificate() == null ? "" : licence.getCertificate());
|
||||
cmd.add(Utils.getSaison() + "");
|
||||
cmd.add(m.getLicence() + "");
|
||||
cmd.add(m.getClub().getName());
|
||||
cmd.add(m.getClub().getNo_affiliation() + "");
|
||||
cmd.add(m.getBirth_date() == null ? "--" : new SimpleDateFormat("dd/MM/yyyy").format(m.getBirth_date()));
|
||||
|
||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(m.getId() + ".");
|
||||
File[] files = new File(media, "ppMembre").listFiles(filter);
|
||||
if (files != null && files.length > 0) {
|
||||
File file = files[0];
|
||||
cmd.add(file.getAbsolutePath());
|
||||
} else {
|
||||
cmd.add("/dev/null");
|
||||
}
|
||||
|
||||
return getPdf(cmd, uuid, LOGGER);
|
||||
}
|
||||
}
|
||||
|
||||
232
src/main/java/fr/titionfire/ffsaf/domain/service/PDFService.java
Normal file
232
src/main/java/fr/titionfire/ffsaf/domain/service/PDFService.java
Normal file
@ -0,0 +1,232 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||
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.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.utils.Utils;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
public class PDFService {
|
||||
private static final Logger LOGGER = Logger.getLogger(PDFService.class);
|
||||
|
||||
@Inject
|
||||
CombRepository combRepository;
|
||||
|
||||
@Inject
|
||||
ClubRepository clubRepository;
|
||||
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
@ConfigProperty(name = "pdf-maker.jar-path")
|
||||
String pdfMakerJarPath;
|
||||
|
||||
@ConfigProperty(name = "pdf-maker.sign-file")
|
||||
String sign_file;
|
||||
|
||||
|
||||
public Uni<Response> getLicencePdf(String subject) {
|
||||
return getLicencePdf(combRepository.find("userId = ?1", subject).firstResult()
|
||||
.call(m -> Mutiny.fetch(m.getLicences())));
|
||||
}
|
||||
|
||||
public Uni<Response> getLicencePdf(Uni<MembreModel> uniBase) {
|
||||
return uniBase
|
||||
.map(Unchecked.function(m -> {
|
||||
LicenceModel licence = m.getLicences().stream()
|
||||
.filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DNotFoundException("Pas de licence pour la saison en cours"));
|
||||
|
||||
try {
|
||||
byte[] buff = make_pdf(m, licence);
|
||||
if (buff == null)
|
||||
throw new IOException("Error making pdf");
|
||||
|
||||
String mimeType = "application/pdf";
|
||||
|
||||
Response.ResponseBuilder resp = Response.ok(buff);
|
||||
resp.type(MediaType.APPLICATION_OCTET_STREAM);
|
||||
resp.header(HttpHeaders.CONTENT_LENGTH, buff.length);
|
||||
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
|
||||
resp.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"inline; " + "filename=\"Attestation d'adhésion " + Utils.getSaison() + "-" +
|
||||
(Utils.getSaison() + 1) + " de " + m.getLname() + " " + m.getFname() + ".pdf\"");
|
||||
return resp.build();
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private byte[] make_pdf(MembreModel m, LicenceModel licence) throws IOException, InterruptedException {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add("java");
|
||||
cmd.add("-jar");
|
||||
cmd.add(pdfMakerJarPath);
|
||||
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
cmd.add("/tmp/" + uuid + ".pdf");
|
||||
cmd.add("membre");
|
||||
cmd.add(m.getFname());
|
||||
cmd.add(m.getLname());
|
||||
cmd.add(m.getGenre().str);
|
||||
cmd.add(m.getCategorie().getName());
|
||||
cmd.add(licence.getCertificate() == null ? "" : licence.getCertificate());
|
||||
cmd.add(Utils.getSaison() + "");
|
||||
cmd.add(m.getLicence() + "");
|
||||
cmd.add(m.getClub().getName());
|
||||
cmd.add(m.getClub().getNo_affiliation() + "");
|
||||
cmd.add(m.getBirth_date() == null ? "--" : new SimpleDateFormat("dd/MM/yyyy").format(m.getBirth_date()));
|
||||
|
||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(m.getId() + ".");
|
||||
File[] files = new File(media, "ppMembre").listFiles(filter);
|
||||
if (files != null && files.length > 0) {
|
||||
File file = files[0];
|
||||
cmd.add(file.getAbsolutePath());
|
||||
} else {
|
||||
cmd.add("/dev/null");
|
||||
}
|
||||
|
||||
return getPdf(cmd, uuid);
|
||||
}
|
||||
|
||||
public Uni<Response> getAffiliationPdf(String subject) {
|
||||
return getAffiliationPdf(
|
||||
combRepository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null || m.getClub() == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
}))
|
||||
.map(MembreModel::getClub)
|
||||
.call(m -> Mutiny.fetch(m.getAffiliations())));
|
||||
}
|
||||
|
||||
public Uni<Response> getAffiliationPdf(long id) {
|
||||
return getAffiliationPdf(
|
||||
clubRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
}))
|
||||
.call(m -> Mutiny.fetch(m.getAffiliations())));
|
||||
}
|
||||
|
||||
|
||||
private Uni<Response> getAffiliationPdf(Uni<ClubModel> uniBase) {
|
||||
return uniBase
|
||||
.map(Unchecked.function(m -> {
|
||||
if (m.getAffiliations().stream()
|
||||
.noneMatch(licenceModel -> licenceModel.getSaison() == Utils.getSaison()))
|
||||
throw new DNotFoundException("Pas d'affiliation pour la saison en cours");
|
||||
|
||||
try {
|
||||
byte[] buff = make_pdf(m);
|
||||
if (buff == null)
|
||||
throw new IOException("Error making pdf");
|
||||
|
||||
String mimeType = "application/pdf";
|
||||
|
||||
Response.ResponseBuilder resp = Response.ok(buff);
|
||||
resp.type(MediaType.APPLICATION_OCTET_STREAM);
|
||||
resp.header(HttpHeaders.CONTENT_LENGTH, buff.length);
|
||||
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
|
||||
resp.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"inline; " + "filename=\"Attestation d'affiliation " + Utils.getSaison() + "-" +
|
||||
(Utils.getSaison() + 1) + " de " + m.getName() + ".pdf\"");
|
||||
return resp.build();
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private byte[] make_pdf(ClubModel m) throws IOException, InterruptedException {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add("java");
|
||||
cmd.add("-jar");
|
||||
cmd.add(pdfMakerJarPath);
|
||||
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
cmd.add("/tmp/" + uuid + ".pdf");
|
||||
cmd.add("club");
|
||||
cmd.add(m.getName());
|
||||
cmd.add(Utils.getSaison() + "");
|
||||
cmd.add(m.getNo_affiliation() + "");
|
||||
cmd.add(new File(sign_file).getAbsolutePath());
|
||||
|
||||
return getPdf(cmd, uuid);
|
||||
}
|
||||
|
||||
static byte[] getPdf(List<String> cmd, UUID uuid) throws IOException, InterruptedException {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
|
||||
processBuilder.redirectErrorStream(true);
|
||||
Process process = processBuilder.start();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
builder.append(line).append("\n");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
|
||||
int code = -1;
|
||||
if (!process.waitFor(30, TimeUnit.SECONDS)) {
|
||||
process.destroy();
|
||||
builder.append("Timeout...");
|
||||
} else {
|
||||
code = process.exitValue();
|
||||
}
|
||||
|
||||
if (t.isAlive())
|
||||
t.interrupt();
|
||||
|
||||
PDFService.LOGGER.debug("PDF maker: " + builder);
|
||||
|
||||
if (code != 0) {
|
||||
throw new IOException("Error code: " + code);
|
||||
} else {
|
||||
File file = new File("/tmp/" + uuid + ".pdf");
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] buff = fis.readAllBytes();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
return buff;
|
||||
} catch (IOException e) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.client.dto.HelloassoNotification;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
@ApplicationScoped
|
||||
public class WebhookService {
|
||||
|
||||
@Inject
|
||||
CheckoutService checkoutService;
|
||||
|
||||
@ConfigProperty(name = "helloasso.organizationSlug")
|
||||
String organizationSlug;
|
||||
|
||||
public Uni<Response> helloAssoNotification(HelloassoNotification notification) {
|
||||
if (notification.getEventType().equals("Payment")){
|
||||
if (notification.getData().getOrder().getOrganizationSlug().equalsIgnoreCase(organizationSlug)){
|
||||
return checkoutService.paymentStatusChange(notification.getData().getState(), notification.getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
return Uni.createFrom().item(Response.ok().build());
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.domain.service.ClubService;
|
||||
import fr.titionfire.ffsaf.domain.service.PDFService;
|
||||
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
||||
import fr.titionfire.ffsaf.rest.data.*;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
@ -39,6 +40,9 @@ public class ClubEndpoints {
|
||||
@Inject
|
||||
ClubService clubService;
|
||||
|
||||
@Inject
|
||||
PDFService pdfService;
|
||||
|
||||
@Inject
|
||||
SecurityCtx securityCtx;
|
||||
|
||||
@ -219,7 +223,7 @@ public class ClubEndpoints {
|
||||
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||
})
|
||||
public Uni<Response> getAffiliation(@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
|
||||
return clubService.getAffiliationPdf(id);
|
||||
return pdfService.getAffiliationPdf(id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -268,7 +272,7 @@ public class ClubEndpoints {
|
||||
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||
})
|
||||
public Uni<Response> getMeAffiliation() {
|
||||
return clubService.getAffiliationPdf(securityCtx.getSubject());
|
||||
return pdfService.getAffiliationPdf(securityCtx.getSubject());
|
||||
}
|
||||
|
||||
@GET
|
||||
|
||||
@ -13,6 +13,7 @@ import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||
|
||||
@ -80,6 +81,21 @@ public class LicenceEndpoints {
|
||||
.map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("pay")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Paiement des licence", description = "Retourne le lien de paiement pour les licence des membre fournie")
|
||||
@APIResponses(value = {
|
||||
@APIResponse(responseCode = "200", description = "Commande avec succès"),
|
||||
@APIResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||
})
|
||||
public Uni<String> payLicences(@Parameter(description = "Id des membres") List<Long> ids) {
|
||||
return licenceService.payLicences(ids, checkPerm, securityCtx);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{id}")
|
||||
@RolesAllowed("federation_admin")
|
||||
@ -97,6 +113,21 @@ public class LicenceEndpoints {
|
||||
return licenceService.setLicence(id, form).map(SimpleLicence::fromModel);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("validate")
|
||||
@RolesAllowed("federation_admin")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Validation licence", description = "Valide en masse les licence de l'année en cours (pour les administrateurs)")
|
||||
@APIResponses(value = {
|
||||
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
|
||||
@APIResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||
})
|
||||
public Uni<?> valideLicences(@Parameter(description = "Id des membres a valider") List<Long> ids) {
|
||||
return licenceService.valideLicences(ids);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@RolesAllowed("federation_admin")
|
||||
|
||||
@ -57,12 +57,14 @@ public class MembreAdminEndpoints {
|
||||
@Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit,
|
||||
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
|
||||
@Parameter(description = "Text à rechercher") @QueryParam("search") String search,
|
||||
@Parameter(description = "Club à filter") @QueryParam("club") String club) {
|
||||
@Parameter(description = "Club à filter") @QueryParam("club") String club,
|
||||
@Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest,
|
||||
@Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) {
|
||||
if (limit == null)
|
||||
limit = 50;
|
||||
if (page == null || page < 1)
|
||||
page = 1;
|
||||
return membreService.searchAdmin(limit, page - 1, search, club);
|
||||
return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment);
|
||||
}
|
||||
|
||||
@GET
|
||||
|
||||
@ -49,12 +49,14 @@ public class MembreClubEndpoints {
|
||||
public Uni<PageResult<SimpleMembre>> getFindClub(
|
||||
@Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit,
|
||||
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
|
||||
@Parameter(description = "Text à rechercher") @QueryParam("search") String search) {
|
||||
@Parameter(description = "Text à rechercher") @QueryParam("search") String search,
|
||||
@Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest,
|
||||
@Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment) {
|
||||
if (limit == null)
|
||||
limit = 50;
|
||||
if (page == null || page < 1)
|
||||
page = 1;
|
||||
return membreService.search(limit, page - 1, search, securityCtx.getSubject());
|
||||
return membreService.search(limit, page - 1, search, licenceRequest, payment, securityCtx.getSubject());
|
||||
}
|
||||
|
||||
@GET
|
||||
|
||||
@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.domain.service.MembreService;
|
||||
import fr.titionfire.ffsaf.domain.service.PDFService;
|
||||
import fr.titionfire.ffsaf.rest.data.MeData;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
@ -34,6 +35,9 @@ public class MembreEndpoints {
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
@Inject
|
||||
PDFService pdfService;
|
||||
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
@ -106,7 +110,7 @@ public class MembreEndpoints {
|
||||
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||
})
|
||||
public Uni<Response> getMeLicence() {
|
||||
return membreService.getLicencePdf(securityCtx.getSubject());
|
||||
return pdfService.getLicencePdf(securityCtx.getSubject());
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -151,6 +155,6 @@ public class MembreEndpoints {
|
||||
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||
})
|
||||
public Uni<Response> getLicencePDF(@PathParam("id") long id) {
|
||||
return membreService.getLicencePdf(membreService.getByIdWithLicence(id).onItem().invoke(checkPerm));
|
||||
return pdfService.getLicencePdf(membreService.getByIdWithLicence(id).onItem().invoke(checkPerm));
|
||||
}
|
||||
}
|
||||
|
||||
53
src/main/java/fr/titionfire/ffsaf/rest/WebhookEndpoints.java
Normal file
53
src/main/java/fr/titionfire/ffsaf/rest/WebhookEndpoints.java
Normal file
@ -0,0 +1,53 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.WebhookService;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.HelloassoNotification;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
@Path("api/webhook")
|
||||
public class WebhookEndpoints {
|
||||
private static final Logger LOGGER = Logger.getLogger(WebhookEndpoints.class);
|
||||
|
||||
@Inject
|
||||
WebhookService webhookService;
|
||||
|
||||
@Inject
|
||||
RoutingContext context;
|
||||
|
||||
@ConfigProperty(name = "helloasso.webhook.ip-source")
|
||||
String helloassoIp;
|
||||
|
||||
@ConfigProperty(name = "quarkus.http.proxy.proxy-address-forwarding")
|
||||
boolean proxyForwarding;
|
||||
|
||||
@POST
|
||||
@Path("ha")
|
||||
@Operation(hidden = true)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Uni<Response> helloAsso(HelloassoNotification notification) {
|
||||
String ip;
|
||||
if (proxyForwarding) {
|
||||
ip = context.request().getHeader("X-Forwarded-For");
|
||||
if (ip == null)
|
||||
ip = context.request().authority().host();
|
||||
} else {
|
||||
ip = context.request().authority().host();
|
||||
}
|
||||
|
||||
if (!helloassoIp.equals(ip)) {
|
||||
LOGGER.infof("helloAsso webhook reject : bas ip (%s)", ip);
|
||||
return Uni.createFrom().item(Response.status(Response.Status.FORBIDDEN).build());
|
||||
}
|
||||
return webhookService.helloAssoNotification(notification);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package fr.titionfire.ffsaf.rest.client;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.client.dto.TokenResponse;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
@Path("/")
|
||||
@RegisterRestClient(configKey = "helloasso-auth")
|
||||
public interface HelloAssoAuthClient {
|
||||
|
||||
@POST
|
||||
@Path("/token")
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Uni<TokenResponse> getToken(
|
||||
@FormParam("grant_type") String grantType,
|
||||
@FormParam("client_id") String clientId,
|
||||
@FormParam("client_secret") String clientSecret
|
||||
);
|
||||
|
||||
@POST
|
||||
@Path("/token")
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Uni<TokenResponse> refreshToken(
|
||||
@FormParam("grant_type") String grantType,
|
||||
@FormParam("client_id") String clientId,
|
||||
@FormParam("refresh_token") String refreshToken
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package fr.titionfire.ffsaf.rest.client;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.HelloAssoTokenService;
|
||||
import io.quarkus.rest.client.reactive.ReactiveClientHeadersFactory;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap;
|
||||
|
||||
@ApplicationScoped
|
||||
public class HelloAssoHeadersFactory extends ReactiveClientHeadersFactory {
|
||||
|
||||
@Inject
|
||||
HelloAssoTokenService helloAssoTokenService;
|
||||
|
||||
@Override
|
||||
public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders,
|
||||
MultivaluedMap<String, String> clientOutgoingHeaders) {
|
||||
MultivaluedMap<String, String> map = new MultivaluedTreeMap<>();
|
||||
return helloAssoTokenService.getValidAccessToken()
|
||||
.invoke(token -> map.putSingle("Authorization", "Bearer " + token)).map(__ -> map);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package fr.titionfire.ffsaf.rest.client;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsRequest;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsResponse;
|
||||
import io.quarkus.rest.client.reactive.ClientExceptionMapper;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Path("/")
|
||||
@RegisterRestClient(configKey = "helloasso-api")
|
||||
@RegisterClientHeaders(HelloAssoHeadersFactory.class)
|
||||
public interface HelloAssoService {
|
||||
|
||||
@GET
|
||||
@Path("/users/me/organizations")
|
||||
@Produces("text/plain")
|
||||
Uni<String> test();
|
||||
|
||||
@POST
|
||||
@Path("organizations/{organizationSlug}/checkout-intents")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Uni<CheckoutIntentsResponse> checkout(@PathParam("organizationSlug") String organizationSlug,
|
||||
CheckoutIntentsRequest data);
|
||||
|
||||
@ClientExceptionMapper
|
||||
static RuntimeException toException(Response response, Method method) {
|
||||
if (!method.getDeclaringClass().getName().equals("fr.titionfire.ffsaf.rest.client.HelloAssoService"))
|
||||
return null;
|
||||
|
||||
if (method.getName().equals("checkout")) {
|
||||
if (response.getStatus() == 400) {
|
||||
if (response.getEntity() instanceof ByteArrayInputStream) {
|
||||
ByteArrayInputStream error = response.readEntity(ByteArrayInputStream.class);
|
||||
return new RuntimeException(new String(error.readAllBytes(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return new RuntimeException("The remote service responded with HTTP 400");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
public class ApiError {
|
||||
public String error;
|
||||
public String error_description;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return error + ": " + error_description;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class CheckoutIntentsRequest {
|
||||
public int totalAmount;
|
||||
public int initialAmount;
|
||||
public String itemName;
|
||||
public String backUrl;
|
||||
public String errorUrl;
|
||||
public String returnUrl;
|
||||
public boolean containsDonation;
|
||||
public Payer payer;
|
||||
public CheckoutMetadata metadata;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public static class Payer {
|
||||
public String firstName;
|
||||
public String lastName;
|
||||
public String email;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class CheckoutIntentsResponse {
|
||||
public int id;
|
||||
public String redirectUrl;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class CheckoutMetadata {
|
||||
public long checkoutDBId;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class HelloassoNotification {
|
||||
private NotificationData data;
|
||||
private String eventType;
|
||||
private CheckoutMetadata metadata;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class NotificationData {
|
||||
private Order order;
|
||||
private Integer id;
|
||||
private String organizationSlug;
|
||||
private String checkoutIntentId;
|
||||
private String oldSlugOrganization; // Pour les changements de nom d'association
|
||||
private String newSlugOrganization;
|
||||
private String state; // Pour les formulaires
|
||||
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public static class Order {
|
||||
private Integer id;
|
||||
private String organizationSlug;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package fr.titionfire.ffsaf.rest.client.dto;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
public class TokenResponse {
|
||||
@JsonProperty("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@JsonProperty("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@JsonProperty("token_type")
|
||||
public String tokenType; // Toujours "bearer"
|
||||
|
||||
@JsonProperty("expires_in")
|
||||
public long expiresIn; // Durée de validité en secondes (1800s = 30min)
|
||||
|
||||
// Pour stocker l'heure d'obtention du token
|
||||
private long timestamp;
|
||||
|
||||
public TokenResponse() {
|
||||
this.timestamp = System.currentTimeMillis() / 1000; // Timestamp en secondes
|
||||
}
|
||||
|
||||
// Vérifie si le token est expiré
|
||||
public boolean isExpired() {
|
||||
return (System.currentTimeMillis() / 1000) - timestamp >= expiresIn;
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,8 @@ public class SimpleLicence {
|
||||
String certificate;
|
||||
@Schema(description = "Validation de la licence", example = "true")
|
||||
boolean validate;
|
||||
@Schema(description = "Licence payer", example = "true")
|
||||
boolean pay;
|
||||
|
||||
public static SimpleLicence fromModel(LicenceModel model) {
|
||||
if (model == null)
|
||||
@ -33,6 +35,7 @@ public class SimpleLicence {
|
||||
.saison(model.getSaison())
|
||||
.certificate(model.getCertificate())
|
||||
.validate(model.isValidate())
|
||||
.pay(model.isPay())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,14 @@ public class LicenceForm {
|
||||
private int saison;
|
||||
|
||||
@FormParam("certificate")
|
||||
@Schema(description = "Nom du médecin sur certificat médical.", example = "M. Jean", required = true)
|
||||
@Schema(description = "Nom et date du médecin sur certificat médical.", example = "M. Jean¤2025-02-03", format = "<Nom>¤<yyyy-mm-dd>", required = true)
|
||||
private String certificate = null;
|
||||
|
||||
@FormParam("validate")
|
||||
@Schema(description = "Licence validée (seuls les admin pourrons enregistrer cette valeur)", example = "true", required = true)
|
||||
@Schema(description = "Licence validée (seuls les admin pourrons modifier cette valeur)", example = "true", required = true)
|
||||
private boolean validate;
|
||||
|
||||
@FormParam("pay")
|
||||
@Schema(description = "Paiement de la licence (seuls les admin pourrons modifier cette valeur)", example = "true", required = true)
|
||||
private boolean pay;
|
||||
}
|
||||
|
||||
@ -63,4 +63,14 @@ quarkus.http.auth.permission.public.policy=permit
|
||||
quarkus.keycloak.admin-client.server-url=https://auth.safca.fr
|
||||
|
||||
|
||||
quarkus.native.resources.includes=asset/**
|
||||
quarkus.native.resources.includes=asset/**
|
||||
|
||||
# HelloAsso Connector
|
||||
helloasso.api=https://api.helloasso.com
|
||||
helloasso.client-id=changeme
|
||||
helloasso.client-secret=changeme
|
||||
|
||||
quarkus.rest-client.helloasso-auth.url=${helloasso.api}/oauth2
|
||||
quarkus.rest-client.helloasso-auth.scope=javax.inject.Singleton
|
||||
|
||||
quarkus.rest-client.helloasso-api.url=${helloasso.api}/v5
|
||||
|
||||
@ -10,6 +10,8 @@ import {apiAxios, errFormater} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SearchBar} from "../components/SearchBar.jsx";
|
||||
import * as XLSX from "xlsx-js-style";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export function MemberList({source}) {
|
||||
const {hash} = useLocation();
|
||||
@ -21,15 +23,17 @@ export function MemberList({source}) {
|
||||
const [licenceData, setLicenceData] = useState([]);
|
||||
const [showLicenceState, setShowLicenceState] = useState(false);
|
||||
const [clubFilter, setClubFilter] = useState("");
|
||||
const [stateFilter, setStateFilter] = useState(4)
|
||||
const [lastSearch, setLastSearch] = useState("");
|
||||
const [paymentFilter, setPaymentFilter] = useState(2);
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}`, setLoading, 1)
|
||||
const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}`);
|
||||
}, [hash, clubFilter]);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
|
||||
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
@ -73,7 +77,6 @@ export function MemberList({source}) {
|
||||
if (search === lastSearch)
|
||||
return;
|
||||
setLastSearch(search);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${search}&club=${clubFilter}`);
|
||||
}
|
||||
|
||||
return <>
|
||||
@ -92,12 +95,20 @@ export function MemberList({source}) {
|
||||
<div className="col-lg-3">
|
||||
<div className="mb-4">
|
||||
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un membre</button>
|
||||
{source === "admin" &&
|
||||
<button className="btn btn-primary" onClick={() => navigate("validate")} style={{marginTop: "0.5rem"}}>Valider des
|
||||
licences</button>}
|
||||
{source === "club" && false && // TODO: enable when payment is ready
|
||||
<button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des
|
||||
licences</button>}
|
||||
</div>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar showLicenceState={showLicenceState} setShowLicenceState={setShowLicenceState} data={data}
|
||||
clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}/>
|
||||
clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
|
||||
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
|
||||
setPaymentFilter={setPaymentFilter}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -356,7 +367,8 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
|
||||
function MakeRow({member, showLicenceState, navigate, source}) {
|
||||
const rowContent = <>
|
||||
<div className="row">
|
||||
<span className="col-auto">{member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------"}</span>
|
||||
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "}
|
||||
{(showLicenceState && member.licence != null && member.licence.pay)? <FontAwesomeIcon icon={faEuroSign}/> : <> </>}</span>
|
||||
<div className="ms-2 col-auto">
|
||||
<div className="fw-bold">{member.fname} {member.lname}</div>
|
||||
</div>
|
||||
@ -381,7 +393,7 @@ function MakeRow({member, showLicenceState, navigate, source}) {
|
||||
|
||||
let allClub = []
|
||||
|
||||
function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source}) {
|
||||
function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) {
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
@ -394,6 +406,25 @@ function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, set
|
||||
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
|
||||
</div>
|
||||
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
|
||||
<option value={4}>Tout les états de licences</option>
|
||||
<option value={0}>Sans demande ni licence validée</option>
|
||||
<option value={1}>Avec demande ou licence validée</option>
|
||||
<option value={2}>Demande en cours</option>
|
||||
<option value={5}>Demande complet</option>
|
||||
<option value={6}>Demande incomplet</option>
|
||||
<option value={3}>Licence validée</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}
|
||||
hidden={stateFilter === 0 || stateFilter === 4}>
|
||||
<option value={2}>Tout les états de paiement</option>
|
||||
<option value={0}>Sans paiement</option>
|
||||
<option value={1}>Avec paiement</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
112
src/main/webapp/src/pages/PayAndValidateList.css
Normal file
112
src/main/webapp/src/pages/PayAndValidateList.css
Normal file
@ -0,0 +1,112 @@
|
||||
.HaPay {
|
||||
width: fit-content;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
}
|
||||
|
||||
.HaPay * {
|
||||
font-family: "Open Sans", "Trebuchet MS", "Lucida Sans Unicode",
|
||||
"Lucida Grande", "Lucida Sans", Arial, sans-serif;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.HaPayButton {
|
||||
align-items: stretch;
|
||||
-webkit-box-pack: stretch;
|
||||
-ms-flex-pack: stretch;
|
||||
background: none;
|
||||
border: none;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.HaPayButton:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.HaPayButton:not(:disabled):focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(73, 211, 138, 0.25);
|
||||
-webkit-box-shadow: 0 0 0 0.25rem rgba(73, 211, 138, 0.25);
|
||||
}
|
||||
|
||||
.HaPayButton:not(:disabled):hover .HaPayButtonLabel,
|
||||
.HaPayButton:not(:disabled):focus .HaPayButtonLabel {
|
||||
background-color: #483dbe;
|
||||
}
|
||||
|
||||
.HaPayButton:not(:disabled):hover .HaPayButtonLogo,
|
||||
.HaPayButton:not(:disabled):focus .HaPayButtonLogo,
|
||||
.HaPayButton:not(:disabled):hover .HaPayButtonLabel,
|
||||
.HaPayButton:not(:disabled):focus .HaPayButtonLabel {
|
||||
border: 1px solid #483dbe;
|
||||
}
|
||||
|
||||
.HaPayButton:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.HaPayButton:disabled .HaPayButtonLogo,
|
||||
.HaPayButton:disabled .HaPayButtonLabel {
|
||||
border: 1px solid #d1d6de;
|
||||
}
|
||||
|
||||
.HaPayButtonLogo {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #4c40cf;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
padding: 7px 10px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.HaPayButtonLabel {
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: space-between;
|
||||
column-gap: 5px;
|
||||
background-color: #4c40cf;
|
||||
border: 1px solid #4c40cf;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.HaPayButton:disabled .HaPayButtonLabel {
|
||||
background-color: #d1d6de;
|
||||
color: #505870;
|
||||
}
|
||||
|
||||
.HaPaySecured {
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: space-between;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #2e2f5e;
|
||||
}
|
||||
|
||||
.HaPay svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
444
src/main/webapp/src/pages/PayAndValidateList.jsx
Normal file
444
src/main/webapp/src/pages/PayAndValidateList.jsx
Normal file
@ -0,0 +1,444 @@
|
||||
import {useLoadingSwitcher} from "../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../hooks/useFetch.js";
|
||||
import {AxiosError} from "../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {apiAxios, errFormater} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SearchBar} from "../components/SearchBar.jsx";
|
||||
import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./PayAndValidateList.css";
|
||||
|
||||
export function PayAndValidateList({source}) {
|
||||
|
||||
const {hash} = useLocation();
|
||||
const navigate = useNavigate();
|
||||
let page = Number(hash.substring(1));
|
||||
page = (page > 0) ? page : 1;
|
||||
|
||||
const [memberData, setMemberData] = useState([]);
|
||||
const [licenceData, setLicenceData] = useState([]);
|
||||
const [clubFilter, setClubFilter] = useState("");
|
||||
const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2)
|
||||
const [lastSearch, setLastSearch] = useState("");
|
||||
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
|
||||
|
||||
const storedMembers = sessionStorage.getItem("selectedMembers");
|
||||
const [selectedMembers, setSelectedMembers] = useState(storedMembers ? JSON.parse(storedMembers) : []);
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
refresh
|
||||
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1)
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers));
|
||||
}, [selectedMembers]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
|
||||
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
const data2 = [];
|
||||
for (const e of data.result) {
|
||||
data2.push({
|
||||
id: e.id,
|
||||
fname: e.fname,
|
||||
lname: e.lname,
|
||||
club: e.club,
|
||||
categorie: e.categorie,
|
||||
licence_number: e.licence,
|
||||
licence: licenceData.find(licence => licence.membre === e.id)
|
||||
})
|
||||
}
|
||||
setMemberData(data2);
|
||||
}, [data, licenceData]);
|
||||
|
||||
useEffect(() => {
|
||||
toast.promise(
|
||||
apiAxios.get(`/licence/current/${source}`),
|
||||
{
|
||||
pending: "Chargement des licences...",
|
||||
success: "Licences chargées",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Impossible de charger les licences")
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
setLicenceData(data.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const search = (search) => {
|
||||
if (search === lastSearch)
|
||||
return;
|
||||
setLastSearch(search);
|
||||
}
|
||||
|
||||
const handleValidation = () => {
|
||||
if (selectedMembers.length === 0) {
|
||||
toast.error("Aucun membre sélectionné");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/validate`, selectedMembers),
|
||||
{
|
||||
pending: "Validation des licences en cours...",
|
||||
success: "Licences validées avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la validation des licences")
|
||||
}
|
||||
}
|
||||
}
|
||||
).then(() => {
|
||||
setSelectedMembers([]);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
|
||||
});
|
||||
}
|
||||
|
||||
const handlePay = () => {
|
||||
if (selectedMembers.length === 0) {
|
||||
toast.error("Aucun membre sélectionné");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/pay`, selectedMembers),
|
||||
{
|
||||
pending: "Création de la commande en cours...",
|
||||
success: "Commande crée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de le création de la commande")
|
||||
}
|
||||
}
|
||||
}
|
||||
).then((data) => {
|
||||
window.location.href = data.data;
|
||||
});
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Validation des licences</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("../member")}>
|
||||
« retour
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-lg-9">
|
||||
<SearchBar search={search}/>
|
||||
{data
|
||||
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate}
|
||||
page={page} source={source} selectedMembers={selectedMembers} setSelectedMembers={setSelectedMembers}/>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
}
|
||||
</div>
|
||||
<div className="col-lg-3">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
|
||||
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
|
||||
setPaymentFilter={setPaymentFilter}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
{source === "admin" && <>
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation">Valider
|
||||
les {selectedMembers.length} licences sélectionnée
|
||||
</button>
|
||||
<ConfirmDialog title="Validation des licences"
|
||||
message={"Êtes-vous sûr de vouloir valider les " + selectedMembers.length + " licences ?"}
|
||||
onConfirm={handleValidation} id="confirm-validation"/>
|
||||
</>}
|
||||
|
||||
{source === "club" && <>
|
||||
<span>{selectedMembers.length} licences sélectionnée<br/>Total à régler : {selectedMembers.length * 15}€</span>
|
||||
<HaPay onClick={handlePay}/>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<FontAwesomeIcon icon={faCircleInfo} size="2xl" style={{color: "#74C0FC", marginRight: "0.25em"}}/>
|
||||
A propos de HelloAsso
|
||||
</div>
|
||||
<div className="card-body">
|
||||
Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à l’association choisie. Vous
|
||||
pouvez soutenir l’aide qu’ils apportent aux associations en laissant une contribution volontaire à HelloAsso au
|
||||
moment de votre paiement.
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function HaPay({onClick}) {
|
||||
return <>
|
||||
<div className="HaPay" style={{marginTop: "0.5em"}}>
|
||||
<button className="HaPayButton" onClick={onClick}>
|
||||
<img
|
||||
src="https://api.helloasso.com/v5/img/logo-ha.svg"
|
||||
alt=""
|
||||
className="HaPayButtonLogo"
|
||||
/>
|
||||
<div className="HaPayButtonLabel">
|
||||
<span> Payer avec </span>
|
||||
<svg
|
||||
width="73"
|
||||
height="14"
|
||||
viewBox="0 0 73 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M72.9992 8.78692C72.9992 11.7371 71.242 13.6283 68.4005 13.6283C65.5964 13.6283 63.8018 11.9073 63.8018 8.74909C63.8018 5.79888 65.559 3.90771 68.4005 3.90771C71.2046 3.90771 72.9992 5.64759 72.9992 8.78692ZM67.2041 8.74909C67.2041 10.5457 67.5779 11.2265 68.4005 11.2265C69.223 11.2265 69.5969 10.5079 69.5969 8.78692C69.5969 6.99031 69.223 6.30949 68.4005 6.30949C67.5779 6.30949 67.1854 7.04705 67.2041 8.74909Z"
|
||||
/>
|
||||
<path
|
||||
d="M62.978 5.08045L61.8003 6.89597C61.1647 6.47991 60.4356 6.25297 59.6692 6.23406C59.1084 6.23406 58.9214 6.40426 58.9214 6.65011C58.9214 6.9527 59.0149 7.08508 60.716 7.61461C62.4172 8.14413 63.3332 8.88169 63.3332 10.527C63.3332 12.3803 61.576 13.6474 59.1084 13.6474C57.5381 13.6474 56.0986 13.0801 55.1826 12.2101L56.7529 10.4514C57.3885 10.962 58.211 11.3402 59.0336 11.3402C59.6131 11.3402 59.9683 11.1511 59.9683 10.7918C59.9683 10.3568 59.7813 10.2622 58.2484 9.78945C56.5847 9.27883 55.65 8.31434 55.65 6.85814C55.65 5.23174 57.0333 3.92684 59.5383 3.92684C60.8656 3.90793 62.1555 4.36181 62.978 5.08045Z"
|
||||
/>
|
||||
<path
|
||||
d="M54.7358 5.08045L53.5581 6.89597C52.9225 6.47991 52.1934 6.25297 51.427 6.23406C50.8662 6.23406 50.6792 6.40426 50.6792 6.65011C50.6792 6.9527 50.7727 7.08508 52.4738 7.61461C54.175 8.14413 55.091 8.88169 55.091 10.527C55.091 12.3803 53.3338 13.6474 50.8662 13.6474C49.2959 13.6474 47.8564 13.0801 46.9404 12.2101L48.5107 10.4514C49.1463 10.962 49.9689 11.3402 50.7914 11.3402C51.3709 11.3402 51.7261 11.1511 51.7261 10.7918C51.7261 10.3568 51.5391 10.2622 50.0062 9.78945C48.3238 9.27883 47.4078 8.31434 47.4078 6.85814C47.4078 5.23174 48.7911 3.92684 51.2961 3.92684C52.6234 3.90793 53.9133 4.36181 54.7358 5.08045Z"
|
||||
/>
|
||||
<path
|
||||
d="M46.7721 11.4156L46.0991 13.5526C44.9401 13.477 44.1923 13.1555 43.6876 12.3045C43.0333 13.3068 42.0051 13.6283 40.9956 13.6283C39.201 13.6283 38.042 12.418 38.042 10.7537C38.042 8.74909 39.5375 7.65222 42.3603 7.65222H42.9959V7.42528C42.9959 6.51752 42.6968 6.27167 41.706 6.27167C40.9209 6.30949 40.1357 6.4797 39.4067 6.74446L38.6963 4.62636C39.8179 4.17248 41.0143 3.94554 42.2294 3.90771C45.0709 3.90771 46.23 5.00459 46.23 7.23616V10.3566C46.23 10.9996 46.3795 11.2643 46.7721 11.4156ZM43.0146 10.7348V9.39209H42.6594C41.7247 9.39209 41.2947 9.71359 41.2947 10.4133C41.2947 10.9239 41.5752 11.2643 42.0238 11.2643C42.4164 11.2643 42.7903 11.0563 43.0146 10.7348Z"
|
||||
/>
|
||||
<path
|
||||
d="M37.5363 8.78692C37.5363 11.7371 35.7791 13.6283 32.9376 13.6283C30.1335 13.6283 28.3389 11.9073 28.3389 8.74909C28.3389 5.79888 30.0961 3.90771 32.9376 3.90771C35.7417 3.90771 37.5363 5.64759 37.5363 8.78692ZM31.7412 8.74909C31.7412 10.5457 32.1151 11.2265 32.9376 11.2265C33.7601 11.2265 34.134 10.5079 34.134 8.78692C34.134 6.99031 33.7601 6.30949 32.9376 6.30949C32.1151 6.30949 31.7225 7.04705 31.7412 8.74909Z"
|
||||
/>
|
||||
<path
|
||||
d="M23.8154 10.6972V0.692948L27.1243 0.352539V10.527C27.1243 10.8296 27.2551 10.9809 27.5355 10.9809C27.6477 10.9809 27.7786 10.962 27.8907 10.9052L28.4889 13.2881C27.8907 13.4961 27.2738 13.6096 26.6569 13.5907C24.8249 13.6285 23.8154 12.5505 23.8154 10.6972Z"
|
||||
/>
|
||||
<path
|
||||
d="M18.8057 10.6972V0.692948L22.1145 0.352539V10.527C22.1145 10.8296 22.2454 10.9809 22.5071 10.9809C22.6192 10.9809 22.7501 10.962 22.8623 10.9052L23.4418 13.2881C22.8436 13.4961 22.2267 13.6096 21.6098 13.5907C19.8151 13.6285 18.8057 12.5505 18.8057 10.6972Z"
|
||||
/>
|
||||
<path
|
||||
d="M17.9071 9.71359H12.4859C12.6728 11.0185 13.3084 11.2454 14.2805 11.2454C14.9161 11.2454 15.533 10.9807 16.2994 10.4511L17.6454 12.2856C16.6172 13.1555 15.3087 13.6283 13.9627 13.6283C10.6912 13.6283 9.13965 11.5858 9.13965 8.78692C9.13965 6.13929 10.6352 3.90771 13.5888 3.90771C16.2247 3.90771 17.9632 5.60976 17.9632 8.63562C17.9819 8.93821 17.9445 9.39209 17.9071 9.71359ZM14.7291 7.70895C14.7105 6.80119 14.5235 6.04473 13.6823 6.04473C12.9719 6.04473 12.6167 6.46079 12.4859 7.84134H14.7291V7.70895Z"
|
||||
/>
|
||||
<path
|
||||
d="M8.24307 6.61229V13.2692H4.93423V7.21746C4.93423 6.49882 4.7286 6.32862 4.4295 6.32862C4.07431 6.32862 3.70043 6.61229 3.30786 7.21746V13.2503H-0.000976562V0.692948L3.30786 0.352539V5.06154C4.07431 4.24834 4.82207 3.90793 5.83154 3.90793C7.32706 3.90793 8.24307 4.89133 8.24307 6.61229Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div className="HaPaySecured">
|
||||
<svg
|
||||
width="9"
|
||||
height="10"
|
||||
viewBox="0 0 11 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.875 3V4.5H7.625V3C7.625 1.96875 6.78125 1.125 5.75 1.125C4.69531 1.125 3.875 1.96875 3.875 3ZM2.75 4.5V3C2.75 1.35938 4.08594 0 5.75 0C7.39062 0 8.75 1.35938 8.75 3V4.5H9.5C10.3203 4.5 11 5.17969 11 6V10.5C11 11.3438 10.3203 12 9.5 12H2C1.15625 12 0.5 11.3438 0.5 10.5V6C0.5 5.17969 1.15625 4.5 2 4.5H2.75ZM1.625 6V10.5C1.625 10.7109 1.78906 10.875 2 10.875H9.5C9.6875 10.875 9.875 10.7109 9.875 10.5V6C9.875 5.8125 9.6875 5.625 9.5 5.625H2C1.78906 5.625 1.625 5.8125 1.625 6Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Paiement sécurisé</span>
|
||||
<img
|
||||
src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-visa.svg"
|
||||
alt="Logo Visa"
|
||||
/>
|
||||
<img
|
||||
src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-mastercard.svg"
|
||||
alt="Logo Mastercard"
|
||||
/>
|
||||
<img
|
||||
src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-cb.svg"
|
||||
alt="Logo CB"
|
||||
/>
|
||||
<img
|
||||
src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-pci.svg"
|
||||
alt="Logo PCI"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, visibleMember, navigate, page, source, selectedMembers, setSelectedMembers}) {
|
||||
const lastCheckedRef = useRef(null);
|
||||
|
||||
function handleCheckbox(e, memberId) {
|
||||
const isShiftKeyPressed = e.shiftKey;
|
||||
const isChecked = !selectedMembers.includes(memberId); // Inverse l'état actuel
|
||||
|
||||
if (isShiftKeyPressed && lastCheckedRef.current !== null) {
|
||||
// Sélection multiple avec Shift
|
||||
const startIndex = visibleMember.findIndex(m => m.id === lastCheckedRef.current);
|
||||
const endIndex = visibleMember.findIndex(m => m.id === memberId);
|
||||
const [start, end] = [Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)];
|
||||
|
||||
const newSelected = [...selectedMembers];
|
||||
for (let i = start; i <= end; i++) {
|
||||
const member = visibleMember[i];
|
||||
if (isChecked && !newSelected.includes(member.id)) {
|
||||
newSelected.push(member.id);
|
||||
} else if (!isChecked) {
|
||||
const index = newSelected.indexOf(member.id);
|
||||
if (index !== -1) newSelected.splice(index, 1);
|
||||
}
|
||||
}
|
||||
setSelectedMembers(newSelected);
|
||||
} else {
|
||||
// Sélection normale (sans Shift)
|
||||
setSelectedMembers(prev =>
|
||||
isChecked
|
||||
? [...prev, memberId]
|
||||
: prev.filter(id => id !== memberId)
|
||||
);
|
||||
}
|
||||
|
||||
lastCheckedRef.current = memberId; // Met à jour le dernier membre cliqué
|
||||
}
|
||||
|
||||
const handleCheckboxClick = (e, memberId) => {
|
||||
handleCheckbox(e, memberId);
|
||||
};
|
||||
|
||||
const handleRowClick = (e, memberId) => {
|
||||
// Si le clic est sur la checkbox, on laisse le gestionnaire de la checkbox gérer l'événement
|
||||
if (e.target.type === 'checkbox') return;
|
||||
handleCheckbox(e, memberId);
|
||||
};
|
||||
|
||||
const pages = []
|
||||
for (let i = 1; i <= data.page_count; i++) {
|
||||
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
|
||||
<span className="page-link" onClick={() => navigate("#" + i)}>{i}</span>
|
||||
</li>);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="mb-4">
|
||||
<small>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})</small>
|
||||
<ul className="list-group">
|
||||
{visibleMember.map(member => (
|
||||
<MakeRow key={member.id} member={member} navigate={navigate} source={source} isChecked={selectedMembers.includes(member.id)}
|
||||
onCheckboxClick={handleCheckboxClick} onRowClick={handleRowClick}/>))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul className="pagination justify-content-center">
|
||||
<li className={"page-item" + ((page <= 1) ? " disabled" : "")}>
|
||||
<span className="page-link" onClick={() => navigate("#" + (page - 1))}>«</span></li>
|
||||
{pages}
|
||||
<li className={"page-item" + ((page >= data.page_count) ? " disabled" : "")}>
|
||||
<span className="page-link" onClick={() => navigate("#" + (page + 1))}>»</span></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
|
||||
const rowContent = <>
|
||||
<div className="row">
|
||||
<div className="col-auto">
|
||||
<input className="form-check-input me-1" type="checkbox" checked={isChecked || false} onChange={() => {
|
||||
}}
|
||||
onClick={(e) => onCheckboxClick(e, member.id)}/>
|
||||
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "}
|
||||
{(member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <> </>}</span>
|
||||
</div>
|
||||
<div className="ms-2 col-auto">
|
||||
<div className="fw-bold">{member.fname} {member.lname}</div>
|
||||
</div>
|
||||
</div>
|
||||
{source === "club" ?
|
||||
<small>{member.categorie}</small>
|
||||
: <small>{member.club?.name || "Sans club"}</small>}
|
||||
</>
|
||||
|
||||
if (member.licence != null) {
|
||||
return <li
|
||||
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-"
|
||||
+ (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))}
|
||||
onClick={(e) => onRowClick(e, member.id)}>
|
||||
{rowContent}
|
||||
</li>
|
||||
} else {
|
||||
return <li className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
|
||||
onClick={(e) => onRowClick(e, member.id)}>
|
||||
{rowContent}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
let allClub = []
|
||||
|
||||
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) {
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
allClub.push(...data.result.map((e) => e.club?.name))
|
||||
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
|
||||
}, [data]);
|
||||
|
||||
return <div>
|
||||
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
|
||||
<option value={1}>Avec demande ou licence validée</option>
|
||||
<option value={2}>Demande en cours</option>
|
||||
<option value={5}>Demande complet</option>
|
||||
<option value={6}>Demande incomplet</option>
|
||||
</select>
|
||||
</div>
|
||||
{source !== "club" && <div className="mb-3">
|
||||
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}>
|
||||
<option value={2}>Tout les états de paiement</option>
|
||||
<option value={0}>Sans paiement</option>
|
||||
<option value={1}>Avec paiement</option>
|
||||
</select>
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
function ClubSelectFilter({clubFilter, setClubFilter}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
|
||||
|
||||
return <>
|
||||
{data
|
||||
? <div className="mb-3">
|
||||
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
||||
<option value="">--- tout les clubs ---</option>
|
||||
<option value="null">--- sans club ---</option>
|
||||
{data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function Def() {
|
||||
return <div className="list-group">
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
</div>
|
||||
}
|
||||
@ -9,8 +9,8 @@ import {AffiliationReqPage} from "./affiliation/AffiliationReqPage.jsx";
|
||||
import {NewClubPage} from "./club/NewClubPage.jsx";
|
||||
import {ClubPage} from "./club/ClubPage.jsx";
|
||||
import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx";
|
||||
import {Scale} from "leaflet/src/control/Control.Scale.js";
|
||||
import {StatsPage} from "./StatsPage.jsx";
|
||||
import {PayAndValidateList} from "../PayAndValidateList.jsx";
|
||||
|
||||
export function AdminRoot() {
|
||||
return <>
|
||||
@ -35,6 +35,10 @@ export function getAdminChildren() {
|
||||
path: 'member/new',
|
||||
element: <NewMemberPage/>
|
||||
},
|
||||
{
|
||||
path: 'member/validate',
|
||||
element: <PayAndValidateList source="admin"/>
|
||||
},
|
||||
{
|
||||
path: 'club',
|
||||
element: <ClubList/>
|
||||
@ -60,4 +64,4 @@ export function getAdminChildren() {
|
||||
element: <StatsPage/>
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ 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 {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
@ -66,7 +66,7 @@ export function LicenceCard({userData}) {
|
||||
return <div key={index}
|
||||
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
|
||||
(licence.validate ? "success" : (licence.certificate?.length > 1 ? "warning" : "danger"))}>
|
||||
<div className="me-auto">{licence?.saison}-{licence?.saison + 1}</div>
|
||||
<div className="me-auto">{licence?.saison}-{licence?.saison + 1} {(licence.pay) && <FontAwesomeIcon icon={faEuroSign}/>}</div>
|
||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||
data-bs-target="#LicenceModal" onClick={_ => setModal(licence)}>
|
||||
<FontAwesomeIcon icon={faPen}/></button>
|
||||
@ -134,6 +134,7 @@ function ModalContent({licence, dispatch}) {
|
||||
const [certificateBy, setCertificateBy] = useState("")
|
||||
const [certificateDate, setCertificateDate] = useState("")
|
||||
const [validate, setValidate] = useState(false)
|
||||
const [pay, setPay] = useState(false)
|
||||
const [isNew, setNew] = useState(true)
|
||||
const setSeason = (event) => {
|
||||
setSaison(Number(event.target.value))
|
||||
@ -147,6 +148,9 @@ function ModalContent({licence, dispatch}) {
|
||||
const handleValidateChange = (event) => {
|
||||
setValidate(event.target.value === 'true');
|
||||
}
|
||||
const handlePayChange = (event) => {
|
||||
setPay(event.target.value === 'true');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (licence.id !== -1) {
|
||||
@ -160,12 +164,14 @@ function ModalContent({licence, dispatch}) {
|
||||
setCertificateDate(licence.certificate.split('¤')[1])
|
||||
}
|
||||
setValidate(licence.validate)
|
||||
setPay(licence.pay);
|
||||
} else {
|
||||
setNew(true)
|
||||
setSaison(getSaison())
|
||||
setCertificateBy("")
|
||||
setCertificateDate("")
|
||||
setValidate(false)
|
||||
setPay(false);
|
||||
}
|
||||
}, [licence]);
|
||||
|
||||
@ -197,6 +203,8 @@ function ModalContent({licence, dispatch}) {
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate"
|
||||
aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/>
|
||||
</div>
|
||||
<RadioGroupeOnOff name="pay" text="Paiement de la licence" value={pay}
|
||||
onChange={handlePayChange}/>
|
||||
<RadioGroupeOnOff name="validate" text="Validation de la licence" value={validate}
|
||||
onChange={handleValidateChange}/>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,9 @@ import {useAuth} from "../../hooks/useAuth.jsx";
|
||||
import {MemberList} from "../MemberList.jsx";
|
||||
import {NewMemberPage} from "./member/NewMemberPage.jsx";
|
||||
import {MyClubPage} from "./club/MyClubPage.jsx";
|
||||
import {PayAndValidateList} from "../PayAndValidateList.jsx";
|
||||
import {PaymentError} from "./PaymentError.jsx";
|
||||
import {PaymentReturn} from "./PaymentReturn.jsx";
|
||||
|
||||
export function ClubRoot() {
|
||||
const {userinfo} = useAuth()
|
||||
@ -33,6 +36,18 @@ export function getClubChildren() {
|
||||
path: 'member',
|
||||
element: <MemberList source="club"/>
|
||||
},
|
||||
{
|
||||
path: 'member/pay',
|
||||
element: <PayAndValidateList source="club"/>
|
||||
},
|
||||
{
|
||||
path: 'member/pay/error',
|
||||
element: <PaymentError/>
|
||||
},
|
||||
{
|
||||
path: 'member/pay/return',
|
||||
element: <PaymentReturn/>
|
||||
},
|
||||
{
|
||||
path: 'member/:id',
|
||||
element: <MemberPage/>
|
||||
@ -46,4 +61,4 @@ export function getClubChildren() {
|
||||
element: <MyClubPage/>
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
19
src/main/webapp/src/pages/club/PaymentError.jsx
Normal file
19
src/main/webapp/src/pages/club/PaymentError.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
|
||||
export function PaymentError () {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const error = searchParams.get("error");
|
||||
|
||||
return <div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-12" style={{textAlign: "center"}}>
|
||||
<h1>Erreur de paiement😕</h1>
|
||||
<p>Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.</p>
|
||||
<p><strong>Message d'erreur :</strong> {error}</p>
|
||||
|
||||
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>Retour à la liste de membres</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
16
src/main/webapp/src/pages/club/PaymentReturn.jsx
Normal file
16
src/main/webapp/src/pages/club/PaymentReturn.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export function PaymentReturn() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return <div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-12" style={{textAlign: "center"}}>
|
||||
<h1>🎉Votre paiement a été traité avec succès.🎉</h1>
|
||||
<p>Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.</p>
|
||||
|
||||
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>Retour à la liste de membres</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@ -2,7 +2,7 @@ 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 {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faEuroSign, faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
@ -172,13 +172,16 @@ function ModalContent({licence, dispatch}) {
|
||||
<span>Certificat médical</span>
|
||||
<div className="input-group mb-3 ">
|
||||
<span className="input-group-text" id="basic-addon2">Fait par</span>
|
||||
<input type="text" className="form-control" placeholder="Fait par" name="certificateBy"
|
||||
<input type="text" className="form-control" placeholder="Fait par" name="certificateBy" disabled={licence.validate}
|
||||
aria-label="Fait par" aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
|
||||
<span className="input-group-text" id="basic-addon2">, le</span>
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate"
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate" disabled={licence.validate}
|
||||
aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<div>Paiement de la licence: <ColoredText boolean={licence.pay}/></div>
|
||||
</div>
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<div>Validation de la licence: <ColoredText boolean={licence.validate}/></div>
|
||||
</div>
|
||||
@ -187,7 +190,7 @@ function ModalContent({licence, dispatch}) {
|
||||
{currentSaison && !licence.validate &&
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>}
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
{currentSaison && !licence.validate && licence.id !== -1 &&
|
||||
{currentSaison && !licence.validate && licence.id !== -1 && !licence.pay &&
|
||||
<button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeLicence(licence.id, dispatch)}>Annuler</button>}
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,9 @@ apiAxios.defaults.headers.post['Accept'] = 'application/json; charset=UTF-8';
|
||||
|
||||
|
||||
export const errFormater = (data, msg) => {
|
||||
return `${msg} (${data.response.statusText}: ${data.response.data}) 😕`
|
||||
if (typeof data.response.data === 'string' || data.response.data instanceof String)
|
||||
return `${msg} (${data.response.statusText}: ${data.response.data}) 😕`
|
||||
return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕`
|
||||
}
|
||||
|
||||
export function getCategoryFormBirthDate(birth_date, currentDate = new Date()) {
|
||||
@ -48,4 +50,4 @@ export function getSaison(currentDate = new Date()) {
|
||||
} else {
|
||||
return currentDate.getFullYear() - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user