Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c553ff14da |
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -59,7 +59,6 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
port: ${{ secrets.SSH_PORT }}
|
port: ${{ secrets.SSH_PORT }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
command_timeout: 20m
|
|
||||||
script: |
|
script: |
|
||||||
cd ${{ secrets.TARGET_DIR }}/ffsaf
|
cd ${{ secrets.TARGET_DIR }}/ffsaf
|
||||||
cp ../vite.env src/main/webapp/.env
|
cp ../vite.env src/main/webapp/.env
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,4 +47,3 @@ nb-configuration.xml
|
|||||||
/cle_prive.jks
|
/cle_prive.jks
|
||||||
/src/main/resources/META-INF/resources/
|
/src/main/resources/META-INF/resources/
|
||||||
/media/
|
/media/
|
||||||
/media-ext/
|
|
||||||
|
|||||||
9
pom.xml
9
pom.xml
@@ -56,10 +56,6 @@
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-resteasy-reactive</artifactId>
|
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.vertx</groupId>
|
<groupId>io.vertx</groupId>
|
||||||
@@ -104,11 +100,6 @@
|
|||||||
<artifactId>jodd-util</artifactId>
|
<artifactId>jodd-util</artifactId>
|
||||||
<version>6.2.1</version>
|
<version>6.2.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-websockets</artifactId>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
@@ -79,11 +79,6 @@ public class ExampleResource {
|
|||||||
response.append("<li>scopes: ").append(this.accessToken.toString()).append("</li>");
|
response.append("<li>scopes: ").append(this.accessToken.toString()).append("</li>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (scopes != null) {
|
|
||||||
response.append("<li>scopes: ").append(this.accessToken.getClaim("user_groups").toString()).append("</li>");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scopes != null) {
|
if (scopes != null) {
|
||||||
response.append("<li>getRoles: ").append(this.securityIdentity.getRoles()).append("</li>");
|
response.append("<li>getRoles: ").append(this.securityIdentity.getRoles()).append("</li>");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.data.model;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@RegisterForReflection
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "affiliation")
|
|
||||||
public class AffiliationModel {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.EAGER)
|
|
||||||
@JoinColumn(name = "club", referencedColumnName = "id")
|
|
||||||
ClubModel club;
|
|
||||||
|
|
||||||
int saison;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.data.model;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@RegisterForReflection
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "affiliation_request")
|
|
||||||
public class AffiliationRequestModel {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
Long id;
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String siren;
|
|
||||||
String RNA;
|
|
||||||
String address;
|
|
||||||
|
|
||||||
String president_lname;
|
|
||||||
String president_fname;
|
|
||||||
String president_email;
|
|
||||||
int president_lincence;
|
|
||||||
|
|
||||||
String tresorier_lname;
|
|
||||||
String tresorier_fname;
|
|
||||||
String tresorier_email;
|
|
||||||
int tresorier_lincence;
|
|
||||||
|
|
||||||
String secretaire_lname;
|
|
||||||
String secretaire_fname;
|
|
||||||
String secretaire_email;
|
|
||||||
int secretaire_lincence;
|
|
||||||
|
|
||||||
int saison;
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -50,7 +49,4 @@ public class ClubModel {
|
|||||||
String no_affiliation;
|
String no_affiliation;
|
||||||
|
|
||||||
boolean international;
|
boolean international;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "club", fetch = FetchType.EAGER)
|
|
||||||
List<AffiliationModel> affiliations;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.data.model;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@RegisterForReflection
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "licence")
|
|
||||||
public class LicenceModel {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "membre", referencedColumnName = "id")
|
|
||||||
MembreModel membre;
|
|
||||||
|
|
||||||
int saison;
|
|
||||||
|
|
||||||
boolean certificate;
|
|
||||||
|
|
||||||
boolean validate;
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import jakarta.persistence.*;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -52,7 +51,4 @@ public class MembreModel {
|
|||||||
GradeArbitrage grade_arbitrage;
|
GradeArbitrage grade_arbitrage;
|
||||||
|
|
||||||
String url_photo;
|
String url_photo;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "membre", fetch = FetchType.LAZY)
|
|
||||||
List<LicenceModel> licences;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.data.repository;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AffiliationRequestRepository implements PanacheRepositoryBase<AffiliationRequestModel, Long> {
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.data.repository;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class LicenceRepository implements PanacheRepositoryBase<LicenceModel, Long> {
|
|
||||||
}
|
|
||||||
@@ -49,4 +49,10 @@ public class ClubEntity {
|
|||||||
.international(model.isInternational())
|
.international(model.isInternational())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClubModel toModel () {
|
||||||
|
return new ClubModel(this.id, this.clubId, this.name, this.country, this.shieldURL, this.contact, this.training_location,
|
||||||
|
this.training_day_time, this.contact_intern, this.RNA, this.SIRET, this.no_affiliation,
|
||||||
|
this.international);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.domain.service;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
|
||||||
import fr.titionfire.ffsaf.data.repository.AffiliationRequestRepository;
|
|
||||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
|
||||||
import fr.titionfire.ffsaf.utils.Utils;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
||||||
|
|
||||||
@WithSession
|
|
||||||
@ApplicationScoped
|
|
||||||
public class AffiliationService {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
CombRepository combRepository;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AffiliationRequestRepository repository;
|
|
||||||
|
|
||||||
@ConfigProperty(name = "upload_dir")
|
|
||||||
String media;
|
|
||||||
|
|
||||||
public Uni<String> save(AffiliationRequestForm form) {
|
|
||||||
AffiliationRequestModel affModel = form.toModel();
|
|
||||||
affModel.setSaison(Utils.getSaison());
|
|
||||||
|
|
||||||
return Uni.createFrom().item(affModel)
|
|
||||||
.call(model -> ((model.getPresident_lincence() != 0) ? combRepository.find("licence",
|
|
||||||
model.getPresident_lincence()).count().invoke(count -> {
|
|
||||||
if (count == 0) {
|
|
||||||
throw new IllegalArgumentException("Licence président inconnue");
|
|
||||||
}
|
|
||||||
}) : Uni.createFrom().nullItem())
|
|
||||||
)
|
|
||||||
.call(model -> ((model.getTresorier_lincence() != 0) ? combRepository.find("licence",
|
|
||||||
model.getTresorier_lincence()).count().invoke(count -> {
|
|
||||||
if (count == 0) {
|
|
||||||
throw new IllegalArgumentException("Licence trésorier inconnue");
|
|
||||||
}
|
|
||||||
}) : Uni.createFrom().nullItem())
|
|
||||||
)
|
|
||||||
.call(model -> ((model.getSecretaire_lincence() != 0) ? combRepository.find("licence",
|
|
||||||
model.getSecretaire_lincence()).count().invoke(count -> {
|
|
||||||
if (count == 0) {
|
|
||||||
throw new IllegalArgumentException("Licence secrétaire inconnue");
|
|
||||||
}
|
|
||||||
}) : Uni.createFrom().nullItem())
|
|
||||||
).chain(model -> Panache.withTransaction(() -> repository.persist(model)))
|
|
||||||
.call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
|
|
||||||
"aff_request/logo")))
|
|
||||||
.call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
|
|
||||||
"aff_request/status")))
|
|
||||||
.map(__ -> "Ok");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,8 @@ package fr.titionfire.ffsaf.domain.service;
|
|||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||||
import fr.titionfire.ffsaf.utils.*;
|
import fr.titionfire.ffsaf.utils.KeycloakException;
|
||||||
|
import fr.titionfire.ffsaf.utils.RequiredAction;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
@@ -20,7 +21,6 @@ import org.keycloak.representations.idm.RoleRepresentation;
|
|||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
import java.text.Normalizer;
|
import java.text.Normalizer;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -47,15 +47,13 @@ public class KeycloakService {
|
|||||||
if (club.getClubId() == null) {
|
if (club.getClubId() == null) {
|
||||||
LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName());
|
LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName());
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||||
GroupRepresentation clubGroup =
|
GroupRepresentation clubGroup = keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
|
||||||
keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
|
|
||||||
.findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
|
.findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
|
||||||
|
|
||||||
GroupRepresentation groupRepresentation = new GroupRepresentation();
|
GroupRepresentation groupRepresentation = new GroupRepresentation();
|
||||||
groupRepresentation.setName(club.getId() + "-" + club.getName());
|
groupRepresentation.setName(club.getId() + "-" + club.getName());
|
||||||
|
|
||||||
try (Response response =
|
try (Response response = keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) {
|
||||||
keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) {
|
|
||||||
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
|
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
|
||||||
throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(),
|
throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(),
|
||||||
response.getStatusInfo().getReasonPhrase()));
|
response.getStatusInfo().getReasonPhrase()));
|
||||||
@@ -83,50 +81,18 @@ public class KeycloakService {
|
|||||||
UserResource user = keycloak.realm(realm).users().get(userId);
|
UserResource user = keycloak.realm(realm).users().get(userId);
|
||||||
user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId()));
|
user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId()));
|
||||||
user.joinGroup(clubId);
|
user.joinGroup(clubId);
|
||||||
LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId,
|
LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, user.toRepresentation().getUsername());
|
||||||
user.toRepresentation().getUsername());
|
|
||||||
return "OK";
|
return "OK";
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<?> setEmail(String userId, String email) {
|
public Uni<UserCompteState> fetchCompte(String id) {
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
|
||||||
UserResource user = keycloak.realm(realm).users().get(userId);
|
|
||||||
UserRepresentation user2 = user.toRepresentation();
|
|
||||||
if (email.equals(user2.getEmail()))
|
|
||||||
return "";
|
|
||||||
user2.setEmail(email);
|
|
||||||
user2.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name()));
|
|
||||||
user.update(user2);
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<?> setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) {
|
|
||||||
List<String> toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire",
|
|
||||||
"asseseur", "arbitre"));
|
|
||||||
List<String> toAdd = new ArrayList<>();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case PRESIDENT -> toAdd.add("club_president");
|
|
||||||
case TRESORIER -> toAdd.add("club_tresorier");
|
|
||||||
case SECRETAIRE -> toAdd.add("club_secretaire");
|
|
||||||
}
|
|
||||||
switch (gradeArbitrage) {
|
|
||||||
case ARBITRE -> toAdd.addAll(List.of("asseseur", "arbitre"));
|
|
||||||
case ASSESSEUR -> toAdd.add("asseseur");
|
|
||||||
}
|
|
||||||
toRemove.removeAll(toAdd);
|
|
||||||
|
|
||||||
return updateRole(id, toAdd, toRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<Pair<UserResource, UserCompteState>> fetchCompte(String id) {
|
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||||
UserResource user = keycloak.realm(realm).users().get(id);
|
UserResource user = keycloak.realm(realm).users().get(id);
|
||||||
UserRepresentation user2 = user.toRepresentation();
|
UserRepresentation user2 = user.toRepresentation();
|
||||||
return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(),
|
return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(),
|
||||||
user2.isEmailVerified()));
|
user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(),
|
||||||
|
user.groups().stream().map(GroupRepresentation::getName).toList());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +104,7 @@ public class KeycloakService {
|
|||||||
public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
|
public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||||
RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel();
|
RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel();
|
||||||
List<RoleRepresentation> roles = keycloak.realm(realm).roles().list();
|
List<RoleRepresentation> roles = keycloak.realm(realm) .roles().list();
|
||||||
resource.add(roles.stream().filter(r -> toAdd.contains(r.getName())).toList());
|
resource.add(roles.stream().filter(r -> toAdd.contains(r.getName())).toList());
|
||||||
resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
|
resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
|
||||||
return "OK";
|
return "OK";
|
||||||
@@ -161,18 +127,9 @@ public class KeycloakService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
|
private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
String login = makeLogin(membreModel);
|
||||||
String login;
|
|
||||||
int i = 1;
|
|
||||||
do {
|
|
||||||
login = makeLogin(membreModel);
|
|
||||||
if (i > 1) {
|
|
||||||
login += i;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
} while (!keycloak.realm(realm).users().searchByUsername(login, true).isEmpty());
|
|
||||||
LOGGER.infof("Creation of user %s...", login);
|
LOGGER.infof("Creation of user %s...", login);
|
||||||
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||||
UserRepresentation user = new UserRepresentation();
|
UserRepresentation user = new UserRepresentation();
|
||||||
user.setUsername(login);
|
user.setUsername(login);
|
||||||
user.setFirstName(membreModel.getFname());
|
user.setFirstName(membreModel.getFname());
|
||||||
@@ -185,37 +142,16 @@ public class KeycloakService {
|
|||||||
|
|
||||||
try (Response response = keycloak.realm(realm).users().create(user)) {
|
try (Response response = keycloak.realm(realm).users().create(user)) {
|
||||||
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
|
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
|
||||||
throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login,
|
throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase()));
|
||||||
response.getStatusInfo().getReasonPhrase()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String finalLogin = login;
|
return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(login)));
|
||||||
return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin)));
|
|
||||||
})
|
})
|
||||||
.invoke(user -> keycloak.realm(realm).users().get(user.getId())
|
|
||||||
.executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(),
|
|
||||||
RequiredAction.UPDATE_PASSWORD.name())))
|
|
||||||
.invoke(user -> membreModel.setUserId(user.getId()))
|
.invoke(user -> membreModel.setUserId(user.getId()))
|
||||||
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
|
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
|
||||||
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
|
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<?> setId(long id, String nid) {
|
|
||||||
return membreService.setUserId(id, nid).map(__ -> "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<?> removeAccount(String userId) {
|
|
||||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
|
||||||
try (Response response = keycloak.realm(realm).users().delete(userId)) {
|
|
||||||
System.out.println(response.getStatusInfo());
|
|
||||||
if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT))
|
|
||||||
throw new KeycloakException("Fail to delete user %s (reason=%s)".formatted(userId,
|
|
||||||
response.getStatusInfo().getReasonPhrase()));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<UserRepresentation> getUser(String username) {
|
private Optional<UserRepresentation> getUser(String username) {
|
||||||
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
|
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
|
||||||
|
|
||||||
@@ -232,6 +168,7 @@ public class KeycloakService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified) {
|
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List<String> realmRoles,
|
||||||
|
List<String> groups) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.domain.service;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
|
||||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
|
||||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
|
||||||
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.LicenceForm;
|
|
||||||
import fr.titionfire.ffsaf.utils.Utils;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.BadRequestException;
|
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
|
||||||
import org.hibernate.reactive.mutiny.Mutiny;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@WithSession
|
|
||||||
@ApplicationScoped
|
|
||||||
public class LicenceService {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
LicenceRepository repository;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
CombRepository combRepository;
|
|
||||||
|
|
||||||
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
|
|
||||||
return combRepository.findById(id).invoke(checkPerm).chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<List<LicenceModel>> getCurrentSaisonLicence(JsonWebToken idToken) {
|
|
||||||
if (idToken == null)
|
|
||||||
return repository.find("saison = ?1", Utils.getSaison()).list();
|
|
||||||
|
|
||||||
return combRepository.find("userId = ?1", idToken.getSubject()).firstResult().map(MembreModel::getClub)
|
|
||||||
.chain(clubModel -> combRepository.find("club = ?1", clubModel).list())
|
|
||||||
.chain(membres -> repository.find("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres).list());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
|
||||||
if (form.getId() == -1) {
|
|
||||||
return combRepository.findById(id).chain(combRepository -> {
|
|
||||||
LicenceModel model = new LicenceModel();
|
|
||||||
model.setMembre(combRepository);
|
|
||||||
model.setSaison(form.getSaison());
|
|
||||||
model.setCertificate(form.isCertificate());
|
|
||||||
model.setValidate(form.isValidate());
|
|
||||||
return Panache.withTransaction(() -> repository.persist(model));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return repository.findById(form.getId()).chain(model -> {
|
|
||||||
model.setCertificate(form.isCertificate());
|
|
||||||
model.setValidate(form.isValidate());
|
|
||||||
return Panache.withTransaction(() -> repository.persist(model));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<?> deleteLicence(long id) {
|
|
||||||
return Panache.withTransaction(() -> repository.deleteById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) {
|
|
||||||
return combRepository.findById(id).invoke(checkPerm).chain(membreModel -> {
|
|
||||||
if (form.getId() == -1) {
|
|
||||||
return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count().invoke(Unchecked.consumer(count -> {
|
|
||||||
if (count > 0)
|
|
||||||
throw new BadRequestException();
|
|
||||||
})).chain(__ -> combRepository.findById(id).chain(combRepository -> {
|
|
||||||
LicenceModel model = new LicenceModel();
|
|
||||||
model.setMembre(combRepository);
|
|
||||||
model.setSaison(Utils.getSaison());
|
|
||||||
model.setCertificate(form.isCertificate());
|
|
||||||
model.setValidate(false);
|
|
||||||
return Panache.withTransaction(() -> repository.persist(model));
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
return repository.findById(form.getId()).chain(model -> {
|
|
||||||
model.setCertificate(form.isCertificate());
|
|
||||||
return Panache.withTransaction(() -> repository.persist(model));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<?> deleteAskLicence(long id, Consumer<MembreModel> checkPerm) {
|
|
||||||
return repository.findById(id)
|
|
||||||
.call(licenceModel -> Mutiny.fetch(licenceModel.getMembre()).invoke(checkPerm))
|
|
||||||
.chain(__ -> Panache.withTransaction(() -> repository.deleteById(id)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,22 @@
|
|||||||
package fr.titionfire.ffsaf.domain.service;
|
package fr.titionfire.ffsaf.domain.service;
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
|
||||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||||
import fr.titionfire.ffsaf.data.repository.ClubRepository;
|
import fr.titionfire.ffsaf.data.repository.ClubRepository;
|
||||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||||
import fr.titionfire.ffsaf.data.repository.LicenceRepository;
|
|
||||||
import fr.titionfire.ffsaf.net2.ServerCustom;
|
import fr.titionfire.ffsaf.net2.ServerCustom;
|
||||||
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
|
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
|
||||||
import fr.titionfire.ffsaf.net2.request.SReqComb;
|
import fr.titionfire.ffsaf.net2.request.SReqComb;
|
||||||
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.ClubMemberForm;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
|
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
|
||||||
import fr.titionfire.ffsaf.utils.*;
|
import fr.titionfire.ffsaf.utils.Pair;
|
||||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||||
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
|
|
||||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||||
import io.quarkus.panache.common.Page;
|
|
||||||
import io.quarkus.panache.common.Sort;
|
import io.quarkus.panache.common.Sort;
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import io.quarkus.vertx.VertxContextSupport;
|
import io.quarkus.vertx.VertxContextSupport;
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.BadRequestException;
|
|
||||||
import jakarta.ws.rs.ForbiddenException;
|
import java.util.List;
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
|
||||||
|
|
||||||
|
|
||||||
@WithSession
|
@WithSession
|
||||||
@@ -36,9 +27,6 @@ public class MembreService {
|
|||||||
CombRepository repository;
|
CombRepository repository;
|
||||||
@Inject
|
@Inject
|
||||||
ClubRepository clubRepository;
|
ClubRepository clubRepository;
|
||||||
@Inject
|
|
||||||
LicenceRepository licenceRepository;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ServerCustom serverCustom;
|
ServerCustom serverCustom;
|
||||||
@Inject
|
@Inject
|
||||||
@@ -46,7 +34,7 @@ public class MembreService {
|
|||||||
|
|
||||||
public SimpleCombModel find(int licence, String np) throws Throwable {
|
public SimpleCombModel find(int licence, String np) throws Throwable {
|
||||||
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
|
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
|
||||||
repository.find("licence = ?1 AND (lname ILIKE ?2 OR fname ILIKE ?2)",
|
repository.find("licence = ?1 AND (lname collate UTF8_GENERAL_CI LIKE ?2 OR fname collate UTF8_GENERAL_CI LIKE ?2)",
|
||||||
licence, np).firstResult().map(SimpleCombModel::fromModel)));
|
licence, np).firstResult().map(SimpleCombModel::fromModel)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,48 +42,8 @@ public class MembreService {
|
|||||||
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel)));
|
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club) {
|
public Uni<List<MembreModel>> getAll() {
|
||||||
if (search == null)
|
return repository.listAll(Sort.ascending("fname", "lname"));
|
||||||
search = "";
|
|
||||||
search = search + "%";
|
|
||||||
|
|
||||||
PanacheQuery<MembreModel> query;
|
|
||||||
|
|
||||||
if (club == null || club.isBlank())
|
|
||||||
query = repository.find("(lname LIKE ?1 OR fname LIKE ?1)",
|
|
||||||
Sort.ascending("fname", "lname"), search).page(Page.ofSize(limit));
|
|
||||||
else
|
|
||||||
query = repository.find("club.name LIKE ?2 AND (lname LIKE ?1 OR fname LIKE ?1)",
|
|
||||||
Sort.ascending("fname", "lname"), search, club + "%").page(Page.ofSize(limit));
|
|
||||||
return getPageResult(query, limit, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, String subject) {
|
|
||||||
if (search == null)
|
|
||||||
search = "";
|
|
||||||
search = search + "%";
|
|
||||||
String finalSearch = search;
|
|
||||||
return repository.find("userId = ?1", subject).firstResult()
|
|
||||||
.chain(membreModel -> {
|
|
||||||
PanacheQuery<MembreModel> query = repository.find("club = ?1 AND (lname LIKE ?2 OR fname LIKE ?2)",
|
|
||||||
Sort.ascending("fname", "lname"), membreModel.getClub(), finalSearch).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))
|
|
||||||
.invoke(result -> result.setPage_size(limit))
|
|
||||||
.call(result -> query.count().invoke(result::setResult_count))
|
|
||||||
.call(result -> query.pageCount()
|
|
||||||
.invoke(Unchecked.consumer(pages -> {
|
|
||||||
if (page > pages) throw new BadRequestException();
|
|
||||||
}))
|
|
||||||
.invoke(result::setPage_count))
|
|
||||||
.call(result -> query.page(Page.of(page, limit)).list()
|
|
||||||
.map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList())
|
|
||||||
.invoke(result::setResult));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<MembreModel> getById(long id) {
|
public Uni<MembreModel> getById(long id) {
|
||||||
@@ -122,117 +70,13 @@ public class MembreService {
|
|||||||
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
.call(membreModel -> (membreModel.getUserId() != null) ?
|
||||||
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem())
|
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem())
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
|
||||||
keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(),
|
|
||||||
membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem())
|
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
|
||||||
keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom().nullItem())
|
|
||||||
.map(__ -> "OK");
|
.map(__ -> "OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uni<String> update(long id, ClubMemberForm membre, JsonWebToken idToken, SecurityIdentity securityIdentity) {
|
|
||||||
return repository.findById(id)
|
|
||||||
.invoke(Unchecked.consumer(membreModel -> {
|
|
||||||
if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}))
|
|
||||||
.invoke(Unchecked.consumer(membreModel -> {
|
|
||||||
RoleAsso source = RoleAsso.MEMBRE;
|
|
||||||
if (securityIdentity.getRoles().contains("club_president")) source = RoleAsso.PRESIDENT;
|
|
||||||
else if (securityIdentity.getRoles().contains("club_secretaire")) source = RoleAsso.SECRETAIRE;
|
|
||||||
else if (securityIdentity.getRoles().contains("club_respo_intra")) source = RoleAsso.SECRETAIRE;
|
|
||||||
if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level > source.level)
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}))
|
|
||||||
.onItem().transformToUni(target -> {
|
|
||||||
target.setFname(membre.getFname());
|
|
||||||
target.setLname(membre.getLname());
|
|
||||||
target.setCountry(membre.getCountry());
|
|
||||||
target.setBirth_date(membre.getBirth_date());
|
|
||||||
target.setGenre(membre.getGenre());
|
|
||||||
target.setCategorie(membre.getCategorie());
|
|
||||||
target.setEmail(membre.getEmail());
|
|
||||||
if (!idToken.getSubject().equals(target.getUserId()))
|
|
||||||
target.setRole(membre.getRole());
|
|
||||||
return Panache.withTransaction(() -> repository.persist(target));
|
|
||||||
})
|
|
||||||
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
|
||||||
keycloakService.setAutoRoleMembre(membreModel.getUserId(), membreModel.getRole(),
|
|
||||||
membreModel.getGrade_arbitrage()) : Uni.createFrom().nullItem())
|
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
|
||||||
keycloakService.setEmail(membreModel.getUserId(), membreModel.getEmail()) : Uni.createFrom().nullItem())
|
|
||||||
.map(__ -> "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<Long> add(FullMemberForm input) {
|
|
||||||
return clubRepository.findById(input.getClub())
|
|
||||||
.chain(clubModel -> {
|
|
||||||
MembreModel model = getMembreModel(input, clubModel);
|
|
||||||
return Panache.withTransaction(() -> repository.persist(model));
|
|
||||||
})
|
|
||||||
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
|
||||||
.map(MembreModel::getId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<Long> add(FullMemberForm input, String subject) {
|
|
||||||
return repository.find("userId = ?1", subject).firstResult()
|
|
||||||
.chain(membreModel -> {
|
|
||||||
MembreModel model = getMembreModel(input, membreModel.getClub());
|
|
||||||
model.setRole(RoleAsso.MEMBRE);
|
|
||||||
model.setGrade_arbitrage(GradeArbitrage.NA);
|
|
||||||
return Panache.withTransaction(() -> repository.persist(model));
|
|
||||||
})
|
|
||||||
.invoke(membreModel -> SReqComb.sendIfNeedAdd(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
|
||||||
.map(MembreModel::getId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<String> delete(long id) {
|
|
||||||
return repository.findById(id)
|
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
|
||||||
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
|
|
||||||
.call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel)))
|
|
||||||
.invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id))
|
|
||||||
.map(__ -> "Ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<String> delete(long id, JsonWebToken idToken) {
|
|
||||||
return repository.findById(id)
|
|
||||||
.invoke(Unchecked.consumer(membreModel -> {
|
|
||||||
if (!GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}))
|
|
||||||
.call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count()
|
|
||||||
.invoke(Unchecked.consumer(l -> {
|
|
||||||
if (l > 0)
|
|
||||||
throw new BadRequestException();
|
|
||||||
})))
|
|
||||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
|
||||||
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
|
|
||||||
.call(membreModel -> Panache.withTransaction(() -> repository.delete(membreModel)))
|
|
||||||
.invoke(membreModel -> SReqComb.sendRm(serverCustom.clients, id))
|
|
||||||
.map(__ -> "Ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uni<?> setUserId(Long id, String id1) {
|
public Uni<?> setUserId(Long id, String id1) {
|
||||||
return repository.findById(id).chain(membreModel -> {
|
return repository.findById(id).chain(membreModel -> {
|
||||||
membreModel.setUserId(id1);
|
membreModel.setUserId(id1);
|
||||||
return Panache.withTransaction(() -> repository.persist(membreModel));
|
return Panache.withTransaction(() -> repository.persist(membreModel));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
|
|
||||||
MembreModel model = new MembreModel();
|
|
||||||
model.setFname(input.getFname());
|
|
||||||
model.setLname(input.getLname());
|
|
||||||
model.setEmail(input.getEmail());
|
|
||||||
model.setGenre(input.getGenre());
|
|
||||||
model.setCountry(input.getCountry());
|
|
||||||
model.setBirth_date(input.getBirth_date());
|
|
||||||
model.setCategorie(input.getCategorie());
|
|
||||||
model.setClub(clubModel);
|
|
||||||
model.setRole(input.getRole());
|
|
||||||
model.setGrade_arbitrage(input.getGrade_arbitrage());
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.net2.packet;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.ws.FileSocket;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
|
||||||
public class RFile {
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(RFile.class);
|
|
||||||
|
|
||||||
final IAction requestSend = (client_Thread, message) -> {
|
|
||||||
try {
|
|
||||||
switch (message.data().get("type").asText()) {
|
|
||||||
case "match":
|
|
||||||
String code = UUID.randomUUID() + "-" + UUID.randomUUID();
|
|
||||||
|
|
||||||
FileSocket.FileRecv fileRecv = new FileSocket.FileRecv(null, message.data().get("name").asText(), null, null,
|
|
||||||
System.currentTimeMillis());
|
|
||||||
FileSocket.sessions.put(code, fileRecv);
|
|
||||||
|
|
||||||
client_Thread.sendRepTo(code, message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
client_Thread.sendErrTo("", message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOGGER.error(e.getMessage(), e);
|
|
||||||
client_Thread.sendErrTo(e.getMessage(), message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void register(HashMap<String, IAction> iMap) {
|
|
||||||
RFile rFile = new RFile();
|
|
||||||
|
|
||||||
iMap.put("requestSend", rFile.requestSend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,5 @@ public class RegisterAction {
|
|||||||
|
|
||||||
RComb.register(iMap);
|
RComb.register(iMap);
|
||||||
RClub.register(iMap);
|
RClub.register(iMap);
|
||||||
RFile.register(iMap);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import jakarta.ws.rs.*;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
|
|
||||||
@Path("api/affiliation")
|
|
||||||
public class AffiliationEndpoints {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("save")
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
|
|
||||||
System.out.println(form);
|
|
||||||
return Uni.createFrom().item("OK");
|
|
||||||
}
|
|
||||||
/*@POST
|
|
||||||
@Path("affiliation")
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
|
|
||||||
System.out.println(form);
|
|
||||||
return service.save(form);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.domain.service.AffiliationService;
|
|
||||||
import fr.titionfire.ffsaf.rest.client.SirenService;
|
|
||||||
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.*;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import jodd.net.MimeTypes;
|
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
||||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
@Path("api/asso")
|
|
||||||
public class AssoEndpoints {
|
|
||||||
|
|
||||||
@RestClient
|
|
||||||
SirenService sirenService;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AffiliationService service;
|
|
||||||
|
|
||||||
@ConfigProperty(name = "upload_dir")
|
|
||||||
String media;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("siren/{siren}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Uni<UniteLegaleRoot> getInfoSiren(@PathParam("siren") String siren) {
|
|
||||||
return sirenService.get_unite(siren).onFailure().transform(throwable -> {
|
|
||||||
if (throwable instanceof WebApplicationException exception) {
|
|
||||||
if (exception.getResponse().getStatus() == 400)
|
|
||||||
return new BadRequestException("Not found");
|
|
||||||
}
|
|
||||||
return throwable;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("affiliation")
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
|
|
||||||
return service.save(form);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@ package fr.titionfire.ffsaf.rest;
|
|||||||
|
|
||||||
import fr.titionfire.ffsaf.domain.service.ClubService;
|
import fr.titionfire.ffsaf.domain.service.ClubService;
|
||||||
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
||||||
import io.quarkus.security.Authenticated;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
@@ -20,7 +20,7 @@ public class ClubEndpoints {
|
|||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/no_detail")
|
@Path("/no_detail")
|
||||||
@Authenticated
|
@RolesAllowed("federation_admin")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Uni<List<SimpleClubModel>> getAll() {
|
public Uni<List<SimpleClubModel>> getAll() {
|
||||||
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList());
|
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList());
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
package fr.titionfire.ffsaf.rest;
|
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.MembreService;
|
||||||
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
||||||
import fr.titionfire.ffsaf.rest.from.ClubMemberForm;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
|
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
|
||||||
import fr.titionfire.ffsaf.utils.GroupeUtils;
|
|
||||||
import fr.titionfire.ffsaf.utils.PageResult;
|
|
||||||
import fr.titionfire.ffsaf.utils.Pair;
|
import fr.titionfire.ffsaf.utils.Pair;
|
||||||
import fr.titionfire.ffsaf.utils.Utils;
|
|
||||||
import io.quarkus.oidc.IdToken;
|
|
||||||
import io.quarkus.security.Authenticated;
|
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
@@ -22,18 +14,16 @@ import jakarta.ws.rs.core.MediaType;
|
|||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jodd.net.MimeTypes;
|
import jodd.net.MimeTypes;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@Authenticated
|
|
||||||
@Path("api/member")
|
@Path("api/member")
|
||||||
public class CombEndpoints {
|
public class CombEndpoints {
|
||||||
|
|
||||||
@@ -43,149 +33,30 @@ public class CombEndpoints {
|
|||||||
@ConfigProperty(name = "upload_dir")
|
@ConfigProperty(name = "upload_dir")
|
||||||
String media;
|
String media;
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IdToken
|
|
||||||
JsonWebToken idToken;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SecurityIdentity securityIdentity;
|
|
||||||
|
|
||||||
Consumer<MembreModel> checkPerm = Unchecked.consumer(membreModel -> {
|
|
||||||
if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
|
||||||
throw new ForbiddenException();
|
|
||||||
});
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/find/admin")
|
@Path("/all")
|
||||||
@RolesAllowed({"federation_admin"})
|
@RolesAllowed("federation_admin")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Uni<PageResult<SimpleMembre>> getFindAdmin(@QueryParam("limit") Integer limit,
|
public Uni<List<SimpleMembre>> getAll() {
|
||||||
@QueryParam("page") Integer page,
|
return membreService.getAll().map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList());
|
||||||
@QueryParam("search") String search,
|
|
||||||
@QueryParam("club") String club) {
|
|
||||||
if (limit == null)
|
|
||||||
limit = 50;
|
|
||||||
if (page == null || page < 1)
|
|
||||||
page = 1;
|
|
||||||
return membreService.searchAdmin(limit, page - 1, search, club);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/find/club")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Uni<PageResult<SimpleMembre>> getFindClub(@QueryParam("limit") Integer limit,
|
|
||||||
@QueryParam("page") Integer page,
|
|
||||||
@QueryParam("search") String search) {
|
|
||||||
if (limit == null)
|
|
||||||
limit = 50;
|
|
||||||
if (page == null || page < 1)
|
|
||||||
page = 1;
|
|
||||||
return membreService.search(limit, page - 1, search, idToken.getSubject());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
@RolesAllowed("federation_admin")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Uni<SimpleMembre> getById(@PathParam("id") long id) {
|
public Uni<SimpleMembre> getById(@PathParam("id") long id) {
|
||||||
return membreService.getById(id).onItem().invoke(checkPerm).map(SimpleMembre::fromModel);
|
return membreService.getById(id).map(SimpleMembre::fromModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@POST
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed({"federation_admin"})
|
@RolesAllowed("federation_admin")
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
public Uni<String> setAdminMembre(@PathParam("id") long id, FullMemberForm input) {
|
public Uni<String> setAdminMembre(@PathParam("id") long id, FullMemberForm input) {
|
||||||
return membreService.update(id, input)
|
Future<String> future = CompletableFuture.supplyAsync(() -> {
|
||||||
.invoke(Unchecked.consumer(out -> {
|
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input.getPhoto_data()))) {
|
||||||
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
|
||||||
})).chain(() -> {
|
|
||||||
if (input.getPhoto_data().length > 0)
|
|
||||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
|
||||||
)).invoke(Unchecked.consumer(out -> {
|
|
||||||
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
|
|
||||||
}));
|
|
||||||
else
|
|
||||||
return Uni.createFrom().nullItem();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@RolesAllowed({"federation_admin"})
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<Long> addAdminMembre(FullMemberForm input) {
|
|
||||||
return membreService.add(input)
|
|
||||||
.invoke(Unchecked.consumer(id -> {
|
|
||||||
if (id == null) throw new InternalError("Fail to creat member data");
|
|
||||||
})).call(id -> {
|
|
||||||
if (input.getPhoto_data().length > 0)
|
|
||||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
|
||||||
));
|
|
||||||
else
|
|
||||||
return Uni.createFrom().nullItem();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("{id}")
|
|
||||||
@RolesAllowed({"federation_admin"})
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
public Uni<String> deleteAdminMembre(@PathParam("id") long id) {
|
|
||||||
return membreService.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PUT
|
|
||||||
@Path("club/{id}")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<String> setMembre(@PathParam("id") long id, ClubMemberForm input) {
|
|
||||||
return membreService.update(id, input, idToken, securityIdentity)
|
|
||||||
.invoke(Unchecked.consumer(out -> {
|
|
||||||
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
|
||||||
})).chain(() -> {
|
|
||||||
if (input.getPhoto_data().length > 0)
|
|
||||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
|
||||||
)).invoke(Unchecked.consumer(out -> {
|
|
||||||
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
|
|
||||||
}));
|
|
||||||
else
|
|
||||||
return Uni.createFrom().nullItem();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("club")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<Long> addMembre(FullMemberForm input) {
|
|
||||||
return membreService.add(input, idToken.getSubject())
|
|
||||||
.invoke(Unchecked.consumer(id -> {
|
|
||||||
if (id == null) throw new InternalError("Fail to creat member data");
|
|
||||||
})).call(id -> {
|
|
||||||
if (input.getPhoto_data().length > 0)
|
|
||||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
|
|
||||||
));
|
|
||||||
else
|
|
||||||
return Uni.createFrom().nullItem();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("club/{id}")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
public Uni<String> deleteMembre(@PathParam("id") long id) {
|
|
||||||
return membreService.delete(id, idToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Future<String> replacePhoto(long id, byte[] input) {
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
|
||||||
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
|
|
||||||
String mimeType = URLConnection.guessContentTypeFromStream(is);
|
String mimeType = URLConnection.guessContentTypeFromStream(is);
|
||||||
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
||||||
if (detectedExtensions.length == 0)
|
if (detectedExtensions.length == 0)
|
||||||
@@ -201,17 +72,33 @@ public class CombEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String extension = "." + detectedExtensions[0];
|
String extension = "." + detectedExtensions[0];
|
||||||
Files.write(new File(media, "ppMembre/" + id + extension).toPath(), input);
|
Files.write(new File(media, "ppMembre/" + input.getId() + extension).toPath(), input.getPhoto_data());
|
||||||
return "OK";
|
return "OK";
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return e.getMessage();
|
return e.getMessage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (input.getPhoto_data().length > 0) {
|
||||||
|
return membreService.update(id, input)
|
||||||
|
.invoke(Unchecked.consumer(out -> {
|
||||||
|
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
||||||
|
}))
|
||||||
|
.chain(() -> Uni.createFrom().future(future))
|
||||||
|
.invoke(Unchecked.consumer(out -> {
|
||||||
|
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
return membreService.update(id, input)
|
||||||
|
.invoke(Unchecked.consumer(out -> {
|
||||||
|
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}/photo")
|
@Path("{id}/photo")
|
||||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
@RolesAllowed("federation_admin")
|
||||||
public Uni<Response> getPhoto(@PathParam("id") long id) throws URISyntaxException {
|
public Uni<Response> getPhoto(@PathParam("id") long id) throws URISyntaxException {
|
||||||
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
|
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
|
||||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
|
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
|
||||||
@@ -229,7 +116,7 @@ public class CombEndpoints {
|
|||||||
|
|
||||||
URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp");
|
URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp");
|
||||||
|
|
||||||
return membreService.getById(id).onItem().invoke(checkPerm).chain(__ -> Uni.createFrom().future(future)
|
return Uni.createFrom().future(future)
|
||||||
.map(filePair -> {
|
.map(filePair -> {
|
||||||
if (filePair == null)
|
if (filePair == null)
|
||||||
return Response.temporaryRedirect(uri).build();
|
return Response.temporaryRedirect(uri).build();
|
||||||
@@ -243,7 +130,7 @@ public class CombEndpoints {
|
|||||||
resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; ");
|
resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; ");
|
||||||
|
|
||||||
return resp.build();
|
return resp.build();
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ package fr.titionfire.ffsaf.rest;
|
|||||||
|
|
||||||
import fr.titionfire.ffsaf.domain.service.KeycloakService;
|
import fr.titionfire.ffsaf.domain.service.KeycloakService;
|
||||||
import fr.titionfire.ffsaf.rest.from.MemberPermForm;
|
import fr.titionfire.ffsaf.rest.from.MemberPermForm;
|
||||||
import fr.titionfire.ffsaf.utils.GroupeUtils;
|
|
||||||
import fr.titionfire.ffsaf.utils.Pair;
|
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
import io.smallrye.mutiny.Uni;
|
||||||
import io.vertx.mutiny.core.Vertx;
|
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.GET;
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
import jakarta.ws.rs.PUT;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,25 +19,11 @@ public class CompteEndpoints {
|
|||||||
@Inject
|
@Inject
|
||||||
KeycloakService service;
|
KeycloakService service;
|
||||||
|
|
||||||
@Inject
|
|
||||||
JsonWebToken accessToken;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SecurityIdentity securityIdentity;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
Vertx vertx;
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
@RolesAllowed("federation_admin")
|
||||||
public Uni<KeycloakService.UserCompteState> getCompte(@PathParam("id") String id) {
|
public Uni<?> getCompte(@PathParam("id") String id) {
|
||||||
return service.fetchCompte(id).call(pair -> vertx.getOrCreateContext().executeBlocking(() -> {
|
return service.fetchCompte(id);
|
||||||
if (!securityIdentity.getRoles().contains("federation_admin") && pair.getKey().groups().stream().map(GroupRepresentation::getPath)
|
|
||||||
.noneMatch(s -> s.startsWith("/club/") && GroupeUtils.contains(s, accessToken)))
|
|
||||||
throw new ForbiddenException();
|
|
||||||
return pair;
|
|
||||||
})).map(Pair::getValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@@ -50,13 +33,6 @@ public class CompteEndpoints {
|
|||||||
return service.initCompte(id);
|
return service.initCompte(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
|
||||||
@Path("{id}/setUUID/{nid}")
|
|
||||||
@RolesAllowed("federation_admin")
|
|
||||||
public Uni<?> initCompte(@PathParam("id") long id, @PathParam("nid") String nid) {
|
|
||||||
return service.setId(id, nid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{id}/roles")
|
@Path("{id}/roles")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed("federation_admin")
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
|
||||||
import fr.titionfire.ffsaf.domain.service.LicenceService;
|
|
||||||
import fr.titionfire.ffsaf.rest.data.SimpleLicence;
|
|
||||||
import fr.titionfire.ffsaf.rest.from.LicenceForm;
|
|
||||||
import fr.titionfire.ffsaf.utils.GroupeUtils;
|
|
||||||
import io.quarkus.oidc.IdToken;
|
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.ws.rs.*;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@Path("api/licence")
|
|
||||||
public class LicenceEndpoints {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
LicenceService licenceService;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IdToken
|
|
||||||
JsonWebToken idToken;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SecurityIdentity securityIdentity;
|
|
||||||
|
|
||||||
Consumer<MembreModel> checkPerm = Unchecked.consumer(membreModel -> {
|
|
||||||
if (!securityIdentity.getRoles().contains("federation_admin") && !GroupeUtils.isInClubGroup(membreModel.getClub().getId(), idToken))
|
|
||||||
throw new ForbiddenException();
|
|
||||||
});
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{id}")
|
|
||||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Uni<List<SimpleLicence>> getLicence(@PathParam("id") long id) {
|
|
||||||
return licenceService.getLicence(id, checkPerm).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("current/admin")
|
|
||||||
@RolesAllowed({"federation_admin"})
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Uni<List<SimpleLicence>> getCurrentSaisonLicenceAdmin() {
|
|
||||||
return licenceService.getCurrentSaisonLicence(null).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("current/club")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Uni<List<SimpleLicence>> getCurrentSaisonLicenceClub() {
|
|
||||||
return licenceService.getCurrentSaisonLicence(idToken).map(licenceModels -> licenceModels.stream().map(SimpleLicence::fromModel).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{id}")
|
|
||||||
@RolesAllowed("federation_admin")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<SimpleLicence> setLicence(@PathParam("id") long id, LicenceForm form) {
|
|
||||||
return licenceService.setLicence(id, form).map(SimpleLicence::fromModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("{id}")
|
|
||||||
@RolesAllowed("federation_admin")
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
public Uni<?> deleteLicence(@PathParam("id") long id) {
|
|
||||||
return licenceService.deleteLicence(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("club/{id}")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
|
||||||
public Uni<SimpleLicence> askLicence(@PathParam("id") long id, LicenceForm form) {
|
|
||||||
return licenceService.askLicence(id, form, checkPerm).map(SimpleLicence::fromModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("club/{id}")
|
|
||||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
public Uni<?> deleteAskLicence(@PathParam("id") long id) {
|
|
||||||
return licenceService.deleteAskLicence(id, checkPerm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest.client;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
|
|
||||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
|
||||||
|
|
||||||
@Path("/")
|
|
||||||
@RegisterRestClient
|
|
||||||
@ClientHeaderParam(name = "X-Client-Secret", value = "${siren-api.key}")
|
|
||||||
public interface SirenService {
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/v3/unites_legales/{SIREN}")
|
|
||||||
Uni<UniteLegaleRoot> get_unite(@PathParam("SIREN") String siren);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest.data;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@RegisterForReflection
|
|
||||||
public class SimpleLicence {
|
|
||||||
Long id;
|
|
||||||
Long membre;
|
|
||||||
int saison;
|
|
||||||
boolean certificate;
|
|
||||||
boolean validate;
|
|
||||||
|
|
||||||
public static SimpleLicence fromModel(LicenceModel model) {
|
|
||||||
if (model == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new SimpleLicenceBuilder()
|
|
||||||
.id(model.getId())
|
|
||||||
.membre(model.getMembre().getId())
|
|
||||||
.saison(model.getSaison())
|
|
||||||
.certificate(model.isCertificate())
|
|
||||||
.validate(model.isValidate())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest.data;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RegisterForReflection
|
|
||||||
public class UniteLegaleRoot {
|
|
||||||
public UniteLegale unite_legale;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RegisterForReflection
|
|
||||||
public static class UniteLegale {
|
|
||||||
public String activite_principale;
|
|
||||||
public Object annee_categorie_entreprise;
|
|
||||||
public Object annee_effectifs;
|
|
||||||
public Object caractere_employeur;
|
|
||||||
public Object categorie_entreprise;
|
|
||||||
public String categorie_juridique;
|
|
||||||
public String date_creation;
|
|
||||||
public String date_debut;
|
|
||||||
public Date date_dernier_traitement;
|
|
||||||
public String denomination;
|
|
||||||
public Object denomination_usuelle_1;
|
|
||||||
public Object denomination_usuelle_2;
|
|
||||||
public Object denomination_usuelle_3;
|
|
||||||
public String economie_sociale_solidaire;
|
|
||||||
public Etablissement etablissement_siege;
|
|
||||||
public ArrayList<Etablissement> etablissements;
|
|
||||||
public String etat_administratif;
|
|
||||||
public String identifiant_association;
|
|
||||||
public String nic_siege;
|
|
||||||
public Object nom;
|
|
||||||
public Object nom_usage;
|
|
||||||
public int nombre_periodes;
|
|
||||||
public String nomenclature_activite_principale;
|
|
||||||
public Object prenom_1;
|
|
||||||
public Object prenom_2;
|
|
||||||
public Object prenom_3;
|
|
||||||
public Object prenom_4;
|
|
||||||
public Object prenom_usuel;
|
|
||||||
public Object pseudonyme;
|
|
||||||
public Object sexe;
|
|
||||||
public Object sigle;
|
|
||||||
public String siren;
|
|
||||||
public String societe_mission;
|
|
||||||
public String statut_diffusion;
|
|
||||||
public Object tranche_effectifs;
|
|
||||||
public Object unite_purgee;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RegisterForReflection
|
|
||||||
public static class Etablissement {
|
|
||||||
private String activite_principale;
|
|
||||||
private Object activite_principale_registre_metiers;
|
|
||||||
private Object annee_effectifs;
|
|
||||||
private String caractere_employeur;
|
|
||||||
private Object code_cedex;
|
|
||||||
private Object code_cedex_2;
|
|
||||||
private String code_commune;
|
|
||||||
private Object code_commune_2;
|
|
||||||
private Object code_pays_etranger;
|
|
||||||
private Object code_pays_etranger_2;
|
|
||||||
private String code_postal;
|
|
||||||
private Object code_postal_2;
|
|
||||||
private Object complement_adresse;
|
|
||||||
private Object complement_adresse2;
|
|
||||||
private String date_creation;
|
|
||||||
private String date_debut;
|
|
||||||
private Date date_dernier_traitement;
|
|
||||||
private Object denomination_usuelle;
|
|
||||||
private Object distribution_speciale;
|
|
||||||
private Object distribution_speciale_2;
|
|
||||||
private Object enseigne_1;
|
|
||||||
private Object enseigne_2;
|
|
||||||
private Object enseigne_3;
|
|
||||||
private boolean etablissement_siege;
|
|
||||||
private String etat_administratif;
|
|
||||||
private Object indice_repetition;
|
|
||||||
private Object indice_repetition_2;
|
|
||||||
private Object libelle_cedex;
|
|
||||||
private Object libelle_cedex_2;
|
|
||||||
private String libelle_commune;
|
|
||||||
private Object libelle_commune_2;
|
|
||||||
private Object libelle_commune_etranger;
|
|
||||||
private Object libelle_commune_etranger_2;
|
|
||||||
private Object libelle_pays_etranger;
|
|
||||||
private Object libelle_pays_etranger_2;
|
|
||||||
private String libelle_voie;
|
|
||||||
private Object libelle_voie_2;
|
|
||||||
private String nic;
|
|
||||||
private int nombre_periodes;
|
|
||||||
private String nomenclature_activite_principale;
|
|
||||||
private String numero_voie;
|
|
||||||
private Object numero_voie_2;
|
|
||||||
private String siren;
|
|
||||||
private String siret;
|
|
||||||
private String statut_diffusion;
|
|
||||||
private Object tranche_effectifs;
|
|
||||||
private String type_voie;
|
|
||||||
private Object type_voie_2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,6 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -21,7 +19,7 @@ public class UserInfo {
|
|||||||
String email;
|
String email;
|
||||||
boolean emailVerified;
|
boolean emailVerified;
|
||||||
long expiration;
|
long expiration;
|
||||||
List<String> groups;
|
Set<String> groups;
|
||||||
Set<String> roles;
|
Set<String> roles;
|
||||||
|
|
||||||
public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) {
|
public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) {
|
||||||
@@ -33,13 +31,7 @@ public class UserInfo {
|
|||||||
builder.email(accessToken.getClaim("email"));
|
builder.email(accessToken.getClaim("email"));
|
||||||
builder.emailVerified(accessToken.getClaim("email_verified"));
|
builder.emailVerified(accessToken.getClaim("email_verified"));
|
||||||
builder.expiration(accessToken.getExpirationTime());
|
builder.expiration(accessToken.getExpirationTime());
|
||||||
List<String> groups = new ArrayList<>();
|
builder.groups(accessToken.getGroups());
|
||||||
if (accessToken.getClaim("user_groups") instanceof Iterable<?>) {
|
|
||||||
for (Object str : (Iterable<?>) accessToken.getClaim("user_groups")) {
|
|
||||||
groups.add(str.toString().substring(1, str.toString().length() - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.groups(groups);
|
|
||||||
builder.roles(securityIdentity.getRoles());
|
builder.roles(securityIdentity.getRoles());
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest.from;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
|
|
||||||
import jakarta.ws.rs.FormParam;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import org.jboss.resteasy.reactive.PartType;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@ToString
|
|
||||||
public class AffiliationRequestForm {
|
|
||||||
@FormParam("name")
|
|
||||||
private String name = null;
|
|
||||||
|
|
||||||
@FormParam("siren")
|
|
||||||
private String siren = null;
|
|
||||||
|
|
||||||
@FormParam("rna")
|
|
||||||
private String rna = null;
|
|
||||||
|
|
||||||
@FormParam("adresse")
|
|
||||||
private String adresse = null;
|
|
||||||
|
|
||||||
@FormParam("status")
|
|
||||||
@PartType(MediaType.APPLICATION_OCTET_STREAM)
|
|
||||||
private byte[] status = new byte[0];
|
|
||||||
|
|
||||||
@FormParam("logo")
|
|
||||||
@PartType(MediaType.APPLICATION_OCTET_STREAM)
|
|
||||||
private byte[] logo = new byte[0];
|
|
||||||
|
|
||||||
@FormParam("president-nom")
|
|
||||||
private String president_lname = null;
|
|
||||||
@FormParam("president-prenom")
|
|
||||||
private String president_fname = null;
|
|
||||||
@FormParam("president-mail")
|
|
||||||
private String president_email = null;
|
|
||||||
@FormParam("president-licence")
|
|
||||||
private String president_lincence = null;
|
|
||||||
|
|
||||||
@FormParam("tresorier-nom")
|
|
||||||
private String tresorier_lname = null;
|
|
||||||
@FormParam("tresorier-prenom")
|
|
||||||
private String tresorier_fname = null;
|
|
||||||
@FormParam("tresorier-mail")
|
|
||||||
private String tresorier_email = null;
|
|
||||||
@FormParam("tresorier-licence")
|
|
||||||
private String tresorier_lincence = null;
|
|
||||||
|
|
||||||
@FormParam("secretaire-nom")
|
|
||||||
private String secretaire_lname = null;
|
|
||||||
@FormParam("secretaire-prenom")
|
|
||||||
private String secretaire_fname = null;
|
|
||||||
@FormParam("secretaire-mail")
|
|
||||||
private String secretaire_email = null;
|
|
||||||
@FormParam("secretaire-licence")
|
|
||||||
private String secretaire_lincence = null;
|
|
||||||
|
|
||||||
public AffiliationRequestModel toModel() {
|
|
||||||
AffiliationRequestModel model = new AffiliationRequestModel();
|
|
||||||
model.setName(this.getName());
|
|
||||||
model.setSiren(this.getSiren());
|
|
||||||
model.setRNA(this.getRna());
|
|
||||||
model.setAddress(this.getAdresse());
|
|
||||||
|
|
||||||
model.setPresident_lname(this.getPresident_lname());
|
|
||||||
model.setPresident_fname(this.getPresident_fname());
|
|
||||||
model.setPresident_email(this.getPresident_email());
|
|
||||||
model.setPresident_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank())
|
|
||||||
? 0 : Integer.parseInt(this.getPresident_lincence()));
|
|
||||||
|
|
||||||
model.setTresorier_lname(this.getTresorier_lname());
|
|
||||||
model.setTresorier_fname(this.getTresorier_fname());
|
|
||||||
model.setTresorier_email(this.getTresorier_email());
|
|
||||||
model.setTresorier_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank())
|
|
||||||
? 0 : Integer.parseInt(this.getTresorier_lincence()));
|
|
||||||
|
|
||||||
model.setSecretaire_lname(this.getSecretaire_lname());
|
|
||||||
model.setSecretaire_fname(this.getSecretaire_fname());
|
|
||||||
model.setSecretaire_email(this.getSecretaire_email());
|
|
||||||
model.setSecretaire_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank())
|
|
||||||
? 0 : Integer.parseInt(this.getSecretaire_lincence()));
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest.from;
|
|
||||||
|
|
||||||
import fr.titionfire.ffsaf.utils.Categorie;
|
|
||||||
import fr.titionfire.ffsaf.utils.Genre;
|
|
||||||
import fr.titionfire.ffsaf.utils.RoleAsso;
|
|
||||||
import jakarta.ws.rs.FormParam;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.jboss.resteasy.reactive.PartType;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class ClubMemberForm {
|
|
||||||
@FormParam("id")
|
|
||||||
private String id = null;
|
|
||||||
|
|
||||||
@FormParam("lname")
|
|
||||||
private String lname = null;
|
|
||||||
|
|
||||||
@FormParam("fname")
|
|
||||||
private String fname = null;
|
|
||||||
|
|
||||||
@FormParam("categorie")
|
|
||||||
private Categorie categorie = null;
|
|
||||||
|
|
||||||
@FormParam("genre")
|
|
||||||
private Genre genre;
|
|
||||||
|
|
||||||
@FormParam("country")
|
|
||||||
private String country;
|
|
||||||
|
|
||||||
@FormParam("birth_date")
|
|
||||||
private Date birth_date;
|
|
||||||
|
|
||||||
@FormParam("email")
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@FormParam("role")
|
|
||||||
private RoleAsso role;
|
|
||||||
|
|
||||||
@FormParam("photo_data")
|
|
||||||
@PartType(MediaType.APPLICATION_OCTET_STREAM)
|
|
||||||
private byte[] photo_data = new byte[0];
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ClubMemberForm{" +
|
|
||||||
"id='" + id + '\'' +
|
|
||||||
", lname='" + lname + '\'' +
|
|
||||||
", fname='" + fname + '\'' +
|
|
||||||
", categorie=" + categorie +
|
|
||||||
", genre=" + genre +
|
|
||||||
", country='" + country + '\'' +
|
|
||||||
", birth_date=" + birth_date +
|
|
||||||
", email='" + email + '\'' +
|
|
||||||
", role=" + role +
|
|
||||||
", url_photo=" + photo_data.length +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.rest.from;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.FormParam;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@ToString
|
|
||||||
public class LicenceForm {
|
|
||||||
@FormParam("id")
|
|
||||||
private long id;
|
|
||||||
|
|
||||||
@FormParam("membre")
|
|
||||||
private long membre;
|
|
||||||
|
|
||||||
@FormParam("saison")
|
|
||||||
private int saison;
|
|
||||||
|
|
||||||
@FormParam("certificate")
|
|
||||||
private boolean certificate;
|
|
||||||
|
|
||||||
@FormParam("validate")
|
|
||||||
private boolean validate;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.utils;
|
|
||||||
|
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
|
||||||
|
|
||||||
public class GroupeUtils {
|
|
||||||
public static boolean isInClubGroup(long id, JsonWebToken accessToken) {
|
|
||||||
if (accessToken.getClaim("user_groups") instanceof Iterable<?>) {
|
|
||||||
for (Object str : (Iterable<?>) accessToken.getClaim("user_groups")) {
|
|
||||||
if (str.toString().substring(1, str.toString().length() - 1).startsWith("/club/" + id + "-"))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean contains(String string, JsonWebToken accessToken) {
|
|
||||||
if (accessToken.getClaim("user_groups") instanceof Iterable<?>) {
|
|
||||||
for (Object str : (Iterable<?>) accessToken.getClaim("user_groups")) {
|
|
||||||
if (str.toString().substring(1, str.toString().length() - 1).contains(string))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.utils;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RegisterForReflection
|
|
||||||
public class PageResult<T> {
|
|
||||||
private int page;
|
|
||||||
private int page_size;
|
|
||||||
private int page_count;
|
|
||||||
private long result_count;
|
|
||||||
private List<T> result = new ArrayList<>();
|
|
||||||
}
|
|
||||||
@@ -4,17 +4,19 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
|
|||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
public enum RoleAsso {
|
public enum RoleAsso {
|
||||||
MEMBRE("Membre", 0),
|
MEMBRE("Membre"),
|
||||||
PRESIDENT("Président", 3),
|
PRESIDENT("Président"),
|
||||||
TRESORIER("Trésorier", 1),
|
TRESORIER("Trésorier"),
|
||||||
SECRETAIRE("Secrétaire", 2);
|
SECRETAIRE("Secrétaire");
|
||||||
|
|
||||||
public final String name;
|
public String name;
|
||||||
public final int level;
|
|
||||||
|
|
||||||
RoleAsso(String name, int level) {
|
RoleAsso(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.level = level;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.utils;
|
|
||||||
|
|
||||||
import jodd.net.MimeTypes;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
public class Utils {
|
|
||||||
|
|
||||||
public static int getSaison() {
|
|
||||||
return getSaison(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getSaison(Date date) {
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTime(date);
|
|
||||||
|
|
||||||
if (calendar.get(Calendar.MONTH) >= Calendar.SEPTEMBER) {
|
|
||||||
return calendar.get(Calendar.YEAR);
|
|
||||||
} else {
|
|
||||||
return calendar.get(Calendar.YEAR) - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Future<String> replacePhoto(long id, byte[] input, String media, String dir) {
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
|
||||||
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
|
|
||||||
String mimeType = URLConnection.guessContentTypeFromStream(is);
|
|
||||||
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
|
||||||
if (detectedExtensions.length == 0)
|
|
||||||
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
|
|
||||||
|
|
||||||
File dirFile = new File(media, dir);
|
|
||||||
if (!dirFile.exists())
|
|
||||||
if (dirFile.mkdirs())
|
|
||||||
throw new IOException("Fail to create directory " + dir);
|
|
||||||
|
|
||||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
|
|
||||||
File[] files = dirFile.listFiles(filter);
|
|
||||||
if (files != null) {
|
|
||||||
for (File file : files) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String extension = "." + detectedExtensions[0];
|
|
||||||
Files.write(new File(dirFile, id + extension).toPath(), input);
|
|
||||||
return "OK";
|
|
||||||
} catch (IOException e) {
|
|
||||||
return e.getMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
package fr.titionfire.ffsaf.ws;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.websocket.*;
|
|
||||||
import jakarta.websocket.server.PathParam;
|
|
||||||
import jakarta.websocket.server.ServerEndpoint;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@ServerEndpoint("/api/ws/file/{code}")
|
|
||||||
@ApplicationScoped
|
|
||||||
public class FileSocket {
|
|
||||||
private static final Logger logger = Logger.getLogger(FileSocket.class);
|
|
||||||
public static Map<String, FileRecv> sessions = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@ConfigProperty(name = "upload_dir")
|
|
||||||
String media;
|
|
||||||
|
|
||||||
/*@Scheduled(every = "10s")
|
|
||||||
void increment() {
|
|
||||||
sessions.forEach((key, value) -> {
|
|
||||||
if (System.currentTimeMillis() - value.time > 60000) {
|
|
||||||
closeAndDelete(value);
|
|
||||||
if (value.session != null && value.session.isOpen()) {
|
|
||||||
try {
|
|
||||||
value.session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "Timeout"));
|
|
||||||
} catch (IOException e) {
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
e.printStackTrace(new PrintWriter(errors));
|
|
||||||
logger.error(errors.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sessions.remove(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@OnOpen
|
|
||||||
public void onOpen(Session session, @PathParam("code") String code) {
|
|
||||||
try {
|
|
||||||
if (sessions.containsKey(code)) {
|
|
||||||
FileRecv fileRecv = sessions.get(code);
|
|
||||||
fileRecv.session = session;
|
|
||||||
fileRecv.file = new File(media + "-ext", "record/" + fileRecv.name);
|
|
||||||
fileRecv.fos = new FileOutputStream(fileRecv.file, false);
|
|
||||||
logger.info("Start reception of file: " + fileRecv.file.getAbsolutePath());
|
|
||||||
} else {
|
|
||||||
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "File not found"));
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
e.printStackTrace(new PrintWriter(errors));
|
|
||||||
logger.error(errors.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClose
|
|
||||||
public void onClose(Session session, @PathParam("code") String code) {
|
|
||||||
if (sessions.containsKey(code)) {
|
|
||||||
FileRecv fileRecv = sessions.get(code);
|
|
||||||
if (fileRecv.fos != null) {
|
|
||||||
try {
|
|
||||||
fileRecv.fos.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
e.printStackTrace(new PrintWriter(errors));
|
|
||||||
logger.error(errors.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info("File received: " + fileRecv.file.getAbsolutePath());
|
|
||||||
sessions.remove(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnError
|
|
||||||
public void onError(Session session, @PathParam("code") String code, Throwable throwable) {
|
|
||||||
if (sessions.containsKey(code)) {
|
|
||||||
closeAndDelete(sessions.get(code));
|
|
||||||
sessions.remove(code);
|
|
||||||
}
|
|
||||||
logger.error("Error on file reception: " + throwable.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OnMessage
|
|
||||||
public void onMessage(String message, @PathParam("code") String code) {
|
|
||||||
if (message.equals("cancel")) {
|
|
||||||
if (sessions.containsKey(code)) {
|
|
||||||
closeAndDelete(sessions.get(code));
|
|
||||||
sessions.remove(code);
|
|
||||||
}
|
|
||||||
logger.error("Error file " + code + " are cancel by the client");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void closeAndDelete(FileRecv fileRecv) {
|
|
||||||
if (fileRecv.fos != null) {
|
|
||||||
try {
|
|
||||||
fileRecv.fos.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
e.printStackTrace(new PrintWriter(errors));
|
|
||||||
logger.error(errors.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fileRecv.file.exists()) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
fileRecv.file.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnMessage
|
|
||||||
public void onMessage(byte[] data, @PathParam("code") String code) {
|
|
||||||
int length = (data[1] << 7) | data[2];
|
|
||||||
|
|
||||||
byte check_sum = 0;
|
|
||||||
for (int j = 3; j < length + 3; j++) {
|
|
||||||
check_sum = (byte) (check_sum ^ data[j]);
|
|
||||||
}
|
|
||||||
// System.out.println(length + " - " + data[1] + " - " + data[0] + " - " + check_sum);
|
|
||||||
|
|
||||||
if (sessions.containsKey(code)) {
|
|
||||||
FileRecv fileRecv = sessions.get(code);
|
|
||||||
|
|
||||||
if (check_sum != data[0]) {
|
|
||||||
fileRecv.session.getAsyncRemote().sendText("Error: Checksum error", result -> {
|
|
||||||
if (result.getException() != null) {
|
|
||||||
logger.error("Unable to send message: " + result.getException());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fileRecv.fos.write(data, 3, length);
|
|
||||||
} catch (IOException e) {
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
e.printStackTrace(new PrintWriter(errors));
|
|
||||||
logger.error(errors.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
fileRecv.session.getAsyncRemote().sendText("ok", result -> {
|
|
||||||
if (result.getException() != null) {
|
|
||||||
logger.error("Unable to send message: " + result.getException());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@RegisterForReflection
|
|
||||||
public static class FileRecv {
|
|
||||||
Session session;
|
|
||||||
String name;
|
|
||||||
File file;
|
|
||||||
FileOutputStream fos;
|
|
||||||
long time;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,16 +34,13 @@ database.port=3306
|
|||||||
database.user=root
|
database.user=root
|
||||||
database.pass=
|
database.pass=
|
||||||
|
|
||||||
siren-api.key=siren-ap
|
|
||||||
quarkus.rest-client."fr.titionfire.ffsaf.rest.client.SirenService".url=https://data.siren-api.fr/
|
|
||||||
|
|
||||||
#Login
|
#Login
|
||||||
quarkus.oidc.token-state-manager.split-tokens=true
|
quarkus.oidc.token-state-manager.split-tokens=true
|
||||||
quarkus.oidc.token.refresh-expired=true
|
quarkus.oidc.token.refresh-expired=true
|
||||||
|
|
||||||
quarkus.oidc.authentication.redirect-path=/api/auth/login
|
quarkus.oidc.authentication.redirect-path=/api/auth/login
|
||||||
quarkus.oidc.logout.path=/api/logout
|
quarkus.oidc.logout.path=/api/logout
|
||||||
quarkus.oidc.logout.post-logout-path=/
|
quarkus.oidc.logout.post-logout-path=/index.html
|
||||||
|
|
||||||
# Only the authenticated users can initiate a logout:
|
# Only the authenticated users can initiate a logout:
|
||||||
quarkus.http.auth.permission.authenticated.paths=api/logout,api/auth/login
|
quarkus.http.auth.permission.authenticated.paths=api/logout,api/auth/login
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {ToastContainer} from "react-toastify";
|
|||||||
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
|
import {ThemeProvider} from "./hooks/useTheme.jsx";
|
||||||
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -27,24 +26,6 @@ const router = createBrowserRouter([
|
|||||||
path: 'admin',
|
path: 'admin',
|
||||||
element: <AdminRoot/>,
|
element: <AdminRoot/>,
|
||||||
children: getAdminChildren()
|
children: getAdminChildren()
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'club',
|
|
||||||
element: <ClubRoot/>,
|
|
||||||
children: getClubChildren()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'affiliation',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
element: <DemandeAff/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'ok',
|
|
||||||
element: <DemandeAffOk/>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -102,7 +83,9 @@ function App() {
|
|||||||
console.log('render')
|
console.log('render')
|
||||||
|
|
||||||
return <KeycloakContextProvider>
|
return <KeycloakContextProvider>
|
||||||
|
<ThemeProvider>
|
||||||
<RouterProvider router={router}/>
|
<RouterProvider router={router}/>
|
||||||
|
</ThemeProvider>
|
||||||
</KeycloakContextProvider>;
|
</KeycloakContextProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,3 @@ export const ColoredCircle = ({color, boolean}) => {
|
|||||||
<span className="colored-circle" style={styles}/>
|
<span className="colored-circle" style={styles}/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => {
|
|
||||||
const styles = {color: '#F00'};
|
|
||||||
|
|
||||||
if (boolean !== undefined) {
|
|
||||||
styles.color = (boolean) ? '#00c700' : '#e50000';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Fragment>
|
|
||||||
<span className="font-weight-bold" style={styles}>{text[boolean]}</span>
|
|
||||||
</Fragment>
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) {
|
|
||||||
return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<div className="modal-header">
|
|
||||||
{title && <h4 className="modal-title" id="myModalLabel">{title}</h4>}
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>Annuler</button>
|
|
||||||
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>Confirmer</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import {getCategoryFormBirthDate} from "../utils/Tools.js";
|
|
||||||
|
|
||||||
export function BirthDayField({inti_date, inti_category}) {
|
|
||||||
const [date, setDate] = useState(inti_date)
|
|
||||||
const [category, setCategory] = useState(inti_category)
|
|
||||||
const [canUpdate, setCanUpdate] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
|
|
||||||
if (b !== canUpdate)
|
|
||||||
setCanUpdate(b)
|
|
||||||
}, [date, category])
|
|
||||||
|
|
||||||
const updateCat = _ => {
|
|
||||||
setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="birth_date">Date de naissance</span>
|
|
||||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
|
|
||||||
name="birth_date" aria-describedby="birth_date" defaultValue={date} required
|
|
||||||
onChange={(e) => setDate(e.target.value)}/>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="category">Catégorie</span>
|
|
||||||
<input type="text" className="form-control" placeholder="" name="category"
|
|
||||||
aria-label="category" value={category} aria-describedby="category"
|
|
||||||
disabled/>
|
|
||||||
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
|
||||||
onClick={updateCat}>Mettre à jours</button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OptionField({name, text, values, value, disabled=false}) {
|
|
||||||
return <div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" id={name}>{text}</label>
|
|
||||||
<select className="form-select" id={name} name={name} defaultValue={value} required disabled={disabled}>
|
|
||||||
{Object.keys(values).map((key, _) => {
|
|
||||||
return (<option key={key} value={key}>{values[key]}</option>)
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TextField({name, text, value, placeholder, type = "text"}) {
|
|
||||||
return <div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id={name}>{text}</span>
|
|
||||||
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
|
|
||||||
name={name} aria-describedby={name} defaultValue={value} required/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckField({name, text, value, row = false}) {
|
|
||||||
return <>{
|
|
||||||
row ?
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div className="form-check">
|
|
||||||
<input className="form-check-input" type="checkbox" id={name} name={name}
|
|
||||||
defaultChecked={value}/>
|
|
||||||
<label className="form-check-label" htmlFor={name}>{text}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: <div className="form-check">
|
|
||||||
<input className="form-check-input" type="checkbox" id={name} name={name} defaultChecked={value}/>
|
|
||||||
<label className="form-check-label" htmlFor={name}>{text}</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Checkbox = ({ label, value, onChange }) => {
|
|
||||||
const handleChange = () => {
|
|
||||||
onChange(!value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className="form-check">
|
|
||||||
<input className="form-check-input" type="checkbox" id="checkbox1" checked={value} onChange={handleChange}/>
|
|
||||||
<label className="form-check-label" htmlFor="checkbox1">{label}</label>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
import LogoIcon from '../assets/FFSSAF-bord-blanc-fond-transparent.webp'
|
import LogoIcon from '../assets/FFSSAF-bord-blanc-fond-transparent.webp'
|
||||||
import './Nav.css'
|
// import './Nav.css'
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
import {useAuth} from "../hooks/useAuth.jsx";
|
import {useAuth} from "../hooks/useAuth.jsx";
|
||||||
import {login, logout} from "../utils/auth.js";
|
import {login, logout} from "../utils/auth.js";
|
||||||
|
import {ThemeCss, useTheme} from "../hooks/useTheme.jsx";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faMoon, faSun} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
|
const themeSwitcherVariant = {
|
||||||
|
light: "outline-primary bg-light",
|
||||||
|
dark: "outline-light bg-dark"
|
||||||
|
};
|
||||||
|
|
||||||
export function Nav() {
|
export function Nav() {
|
||||||
|
const {theme} = useTheme();
|
||||||
|
|
||||||
return <nav className="navbar navbar-light navbar-expand-md bg-body-tertiary " id="main-navbar">
|
return <>
|
||||||
|
<ThemeCss path="./nav"/>
|
||||||
|
<nav className={"navbar navbar-" + theme + " navbar-expand-md bg-body-tertiary bg-" + theme}
|
||||||
|
id="main-navbar">
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<a className="navbar-brand" href="/">
|
<a className="navbar-brand" href="/">
|
||||||
<img className="logo" src={LogoIcon} alt="logo"/>
|
<img className="logo" src={LogoIcon} alt="logo"/>
|
||||||
@@ -18,50 +30,24 @@ export function Nav() {
|
|||||||
aria-expanded="false" aria-label="Toggle navigation">
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span className="navbar-toggler-icon"></span>
|
<span className="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div className="collapse navbar-collapse" id="navbarNavDropdown">
|
<div className={"collapse navbar-collapse bg-" + theme} id="navbarNavDropdown">
|
||||||
<div className="collapse-item">
|
<div className="collapse-item">
|
||||||
<ul className="navbar-nav">
|
<ul className="navbar-nav">
|
||||||
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
|
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
|
||||||
<ClubMenu/>
|
|
||||||
<AdminMenu/>
|
<AdminMenu/>
|
||||||
<AffiliationMenu/>
|
|
||||||
<LoginMenu/>
|
<LoginMenu/>
|
||||||
|
<li className="nav-item"><ThemeSwitcher/></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
</>
|
||||||
|
|
||||||
|
|
||||||
function AffiliationMenu() {
|
|
||||||
const {is_authenticated} = useAuth()
|
|
||||||
|
|
||||||
if (is_authenticated)
|
|
||||||
return <></>
|
|
||||||
return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">Demande d'affiliation</NavLink></li>
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClubMenu() {
|
|
||||||
const {is_authenticated, userinfo} = useAuth()
|
|
||||||
|
|
||||||
if (!is_authenticated || !(userinfo?.roles?.includes("club_president")
|
|
||||||
|| userinfo?.roles?.includes("club_secretaire") || userinfo?.roles?.includes("club_tresorier")))
|
|
||||||
return <></>
|
|
||||||
|
|
||||||
return <li className="nav-item dropdown">
|
|
||||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
Club
|
|
||||||
</div>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Member</NavLink></li>
|
|
||||||
<li className="nav-item"><NavLink className="nav-link" to="/club/b">B</NavLink></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AdminMenu() {
|
function AdminMenu() {
|
||||||
const {is_authenticated, userinfo} = useAuth()
|
const {is_authenticated, userinfo} = useAuth()
|
||||||
|
const {theme} = useTheme();
|
||||||
|
|
||||||
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
|
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
|
||||||
return <></>
|
return <></>
|
||||||
@@ -70,7 +56,7 @@ function AdminMenu() {
|
|||||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
Administration
|
Administration
|
||||||
</div>
|
</div>
|
||||||
<ul className="dropdown-menu">
|
<ul className={"dropdown-menu " + themeSwitcherVariant[theme]}>
|
||||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Member</NavLink></li>
|
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Member</NavLink></li>
|
||||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/b">B</NavLink></li>
|
<li className="nav-item"><NavLink className="nav-link" to="/admin/b">B</NavLink></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -88,3 +74,9 @@ function LoginMenu() {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ThemeSwitcher() {
|
||||||
|
const {isLight, toggleTheme} = useTheme()
|
||||||
|
return <div className="nav-link" onClick={toggleTheme}>{isLight ? <FontAwesomeIcon icon={faSun}/> :
|
||||||
|
<FontAwesomeIcon icon={faMoon}/>}</div>
|
||||||
|
}
|
||||||
@@ -19,16 +19,12 @@ export function useFetch(url, setLoading = null, loadingLevel = 1, config = {})
|
|||||||
const [data, setData] = useState(null)
|
const [data, setData] = useState(null)
|
||||||
const [error, setErrors] = useState(null)
|
const [error, setErrors] = useState(null)
|
||||||
|
|
||||||
const refresh = (url) => {
|
|
||||||
stdAction(apiAxios.get(url, config), setData, setErrors, setLoading, loadingLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh(url)
|
stdAction(apiAxios.get(url, config), setData, setErrors, setLoading, loadingLevel)
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data, error, refresh
|
data, error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
42
src/main/webapp/src/hooks/useTheme.jsx
Normal file
42
src/main/webapp/src/hooks/useTheme.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React, {createContext, useContext, useLayoutEffect, useState} from "react";
|
||||||
|
|
||||||
|
export const ThemeContext = createContext({
|
||||||
|
theme: 'light',
|
||||||
|
toggleTheme: () => {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const {theme, toggleTheme} = useContext(ThemeContext);
|
||||||
|
return {
|
||||||
|
isLight: theme === 'light',
|
||||||
|
isDark: theme === 'dark',
|
||||||
|
theme,
|
||||||
|
toggleTheme
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeCss({path}) {
|
||||||
|
const {theme} = useTheme();
|
||||||
|
|
||||||
|
const Light = React.lazy(() => import(path + ".css"));
|
||||||
|
const Dark = React.lazy(() => import(path + ".dark.css"));
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<React.Suspense fallback={<></>}>
|
||||||
|
{theme === "light" && <Light />}
|
||||||
|
{theme === "dark" && <Dark />}
|
||||||
|
</React.Suspense>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({children}) {
|
||||||
|
const [theme, setTheme] = useState('light');
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(theme === 'light' ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={{theme, toggleTheme}}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
}
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import {useState} from "react";
|
|
||||||
import {apiAxios} from "../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {useNavigate} from "react-router-dom";
|
|
||||||
|
|
||||||
function reconstruireAdresse(infos) {
|
|
||||||
let adresseReconstruite = "";
|
|
||||||
adresseReconstruite += infos.numero_voie + ' ' + infos.type_voie + ' ';
|
|
||||||
adresseReconstruite += infos.libelle_voie + ', ';
|
|
||||||
adresseReconstruite += infos.code_postal + ' ' + infos.libelle_commune + ', ';
|
|
||||||
|
|
||||||
if (infos.complement_adresse) {
|
|
||||||
adresseReconstruite += infos.complement_adresse + ', ';
|
|
||||||
}
|
|
||||||
if (infos.code_cedex && infos.libelle_cedex) {
|
|
||||||
adresseReconstruite += 'Cedex ' + infos.code_cedex + ' - ' + infos.libelle_cedex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adresseReconstruite.endsWith(', ')) {
|
|
||||||
adresseReconstruite = adresseReconstruite.slice(0, -2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return adresseReconstruite;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function DemandeAff() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const submit = (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
const formData = new FormData(event.target)
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.post(`asso/affiliation`, formData, { headers: {'Accept': '*/*'}}),
|
|
||||||
{
|
|
||||||
pending: "Enregistrement de la demande d'affiliation en cours",
|
|
||||||
success: "Demande d'affiliation enregistrée avec succès 🎉",
|
|
||||||
error: "Échec de la demande d'affiliation 😕"
|
|
||||||
}
|
|
||||||
).then(_ => {
|
|
||||||
// navigate("/affiliation/ok")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
<h1>Demande d'affiliation</h1>
|
|
||||||
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année
|
|
||||||
suivante.</p>
|
|
||||||
Pour s’affilier, une association sportive doit réunir les conditions suivantes :
|
|
||||||
<ul>
|
|
||||||
<li>Avoir son siège social en France ou Principauté de Monaco</li>
|
|
||||||
<li>Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport</li>
|
|
||||||
<li>Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération</li>
|
|
||||||
<li>Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la
|
|
||||||
Fédération
|
|
||||||
</li>
|
|
||||||
<li>Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute
|
|
||||||
discrimination
|
|
||||||
</li>
|
|
||||||
<li>Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la
|
|
||||||
Fédération
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className="card mb-4">
|
|
||||||
<form onSubmit={submit}>
|
|
||||||
<div className="card-body">
|
|
||||||
<h4>L'association</h4>
|
|
||||||
<AssoInfo/>
|
|
||||||
<h4>Le président</h4>
|
|
||||||
<MembreInfo role="president"/>
|
|
||||||
<h4>Le trésorier</h4>
|
|
||||||
<MembreInfo role="tresorier"/>
|
|
||||||
<h4>Le secrétaire</h4>
|
|
||||||
<MembreInfo role="secretaire"/>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<p>Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour
|
|
||||||
accéder à votre espace FFSAF</p>
|
|
||||||
Notez que pour finaliser votre affiliation, il vous faudra :
|
|
||||||
<ul>
|
|
||||||
<li>Disposer d’au moins trois membres licenciés, dont le président, le trésorier et le
|
|
||||||
secrétaire
|
|
||||||
</li>
|
|
||||||
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-md-center">
|
|
||||||
<button type="submit" className="btn btn-primary">Confirmer ma demande d'affiliation
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function AssoInfo() {
|
|
||||||
const [denomination, setDenomination] = useState("")
|
|
||||||
const [siren, setSiren] = useState("")
|
|
||||||
const [rna, setRna] = useState("")
|
|
||||||
const [rnaEnable, setRnaEnable] = useState(false)
|
|
||||||
const [adresse, setAdresse] = useState("")
|
|
||||||
|
|
||||||
const fetchSiren = () => {
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.get(`asso/siren/${siren}`),
|
|
||||||
{
|
|
||||||
pending: "Recherche de l'association en cours",
|
|
||||||
success: "Association trouvée avec succès 🎉",
|
|
||||||
error: "Échec de la recherche de l'association 😕"
|
|
||||||
}
|
|
||||||
).then(data => {
|
|
||||||
const data2 = data.data.unite_legale
|
|
||||||
setDenomination(data2.denomination)
|
|
||||||
setRnaEnable(data2.identifiant_association === null)
|
|
||||||
setRna(data2.identifiant_association ? data2.identifiant_association : "")
|
|
||||||
setAdresse(reconstruireAdresse(data2.etablissement_siege))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span>
|
|
||||||
<input type="text" className="form-control" placeholder="Nom de l'association" name="name"
|
|
||||||
aria-label="Nom de l'association"
|
|
||||||
aria-describedby="basic-addon1" required/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text">N° SIREN*</span>
|
|
||||||
<input type="number" className="form-control" placeholder="siren" name="siren" required value={siren}
|
|
||||||
onChange={e => setSiren(e.target.value)}/>
|
|
||||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
|
|
||||||
onClick={fetchSiren}>Rechercher
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="basic-addon1">Dénomination</span>
|
|
||||||
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter"
|
|
||||||
aria-label="Dénomination"
|
|
||||||
aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="basic-addon1">RNA</span>
|
|
||||||
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA"
|
|
||||||
aria-describedby="basic-addon1"
|
|
||||||
disabled={!rnaEnable} name="rna" value={rna} onChange={e => setRna(e.target.value)}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<span className="input-group-text" id="basic-addon1">Adresse*</span>
|
|
||||||
<input type="text" className="form-control" placeholder="Adresse" aria-label="Adresse"
|
|
||||||
aria-describedby="basic-addon1"
|
|
||||||
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="status">Status*</label>
|
|
||||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="logo">Logo*</label>
|
|
||||||
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg"
|
|
||||||
required/>
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MembreInfo({role}) {
|
|
||||||
return <div className="row g-3 mb-3">
|
|
||||||
<div className="col-sm-3">
|
|
||||||
<div className="form-floating">
|
|
||||||
<input type="text" className="form-control" id="floatingInput" placeholder="Nom" name={role + "-nom"}/>
|
|
||||||
<label htmlFor="floatingInput">Nom</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-3">
|
|
||||||
<div className="form-floating">
|
|
||||||
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom"
|
|
||||||
name={role + "-prenom"}/>
|
|
||||||
<label htmlFor="floatingInput">Prénom</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-5">
|
|
||||||
<div className="form-floating">
|
|
||||||
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com"
|
|
||||||
name={role + "-mail"}/>
|
|
||||||
<label htmlFor="floatingInput">Email</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-3">
|
|
||||||
<div>OU</div>
|
|
||||||
<div className="form-floating">
|
|
||||||
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
|
|
||||||
name={role + "-licence"}/>
|
|
||||||
<label htmlFor="floatingInput">N° Licence</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DemandeAffOk() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-green-800 text-4xl">Demande d'affiliation envoyée avec succès</h1>
|
|
||||||
<p>Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre
|
|
||||||
espace FFSAF</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
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, useState} from "react";
|
|
||||||
import {Input} from "../components/Input.jsx";
|
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
|
||||||
import {Checkbox} from "../components/MemberCustomFiels.jsx";
|
|
||||||
import axios from "axios";
|
|
||||||
import {apiAxios} from "../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
|
|
||||||
const removeDiacritics = str => {
|
|
||||||
return str
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MemberList({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 [showLicenceState, setShowLicenceState] = useState(false);
|
|
||||||
const [clubFilter, setClubFilter] = useState("");
|
|
||||||
const [lastSearch, setLastSearch] = useState("");
|
|
||||||
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}`, setLoading, 1)
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}`);
|
|
||||||
}, [hash, clubFilter]);
|
|
||||||
|
|
||||||
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,
|
|
||||||
licence_number: e.licence,
|
|
||||||
licence: showLicenceState ? licenceData.find(licence => licence.membre === e.id) : null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setMemberData(data2);
|
|
||||||
}, [data, licenceData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showLicenceState)
|
|
||||||
return;
|
|
||||||
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.get(`/licence/current/${source}`),
|
|
||||||
{
|
|
||||||
pending: "Chargement des licences...",
|
|
||||||
success: "Licences chargées",
|
|
||||||
error: "Impossible de charger les licences"
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
setLicenceData(data.data);
|
|
||||||
});
|
|
||||||
}, [showLicenceState]);
|
|
||||||
|
|
||||||
const search = (search) => {
|
|
||||||
if (search === lastSearch)
|
|
||||||
return;
|
|
||||||
setLastSearch(search);
|
|
||||||
refresh(`/member/find/${source}?page=${page}&search=${search}&club=${clubFilter}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-lg-9">
|
|
||||||
<SearchBar search={search}/>
|
|
||||||
{data
|
|
||||||
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState}
|
|
||||||
page={page}/>
|
|
||||||
: error
|
|
||||||
? <AxiosError error={error}/>
|
|
||||||
: <Def/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-3">
|
|
||||||
<div className="mb-4">
|
|
||||||
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un membre</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}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchBar({search}) {
|
|
||||||
const [searchInput, setSearchInput] = useState("");
|
|
||||||
|
|
||||||
const handelChange = (e) => {
|
|
||||||
setSearchInput(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
searchMember();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchMember = () => {
|
|
||||||
search(removeDiacritics(searchInput));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const delayDebounceFn = setTimeout(() => {
|
|
||||||
searchMember();
|
|
||||||
}, 750)
|
|
||||||
return () => clearTimeout(delayDebounceFn)
|
|
||||||
}, [searchInput])
|
|
||||||
|
|
||||||
return <div className="mb-3">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<input type="text" className="form-control" placeholder="Rechercher..." aria-label="Rechercher..."
|
|
||||||
aria-describedby="button-addon2" value={searchInput} onChange={handelChange} onKeyDown={handleKeyDown}/>
|
|
||||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
|
|
||||||
onClick={searchMember}>Rechercher
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page}) {
|
|
||||||
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>
|
|
||||||
<div className="list-group">
|
|
||||||
{visibleMember.map(member => (<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState}/>))}
|
|
||||||
</div>
|
|
||||||
</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, showLicenceState, navigate}) {
|
|
||||||
const rowContent = <>
|
|
||||||
<div className="row">
|
|
||||||
<span className="col-auto">{String(member.licence_number).padStart(5, '0')}</span>
|
|
||||||
<div className="ms-2 col-auto">
|
|
||||||
<div className="fw-bold">{member.fname} {member.lname}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small>{member.club?.name || "Sans club"}</small>
|
|
||||||
</>
|
|
||||||
|
|
||||||
if (showLicenceState && member.licence != null) {
|
|
||||||
return <div
|
|
||||||
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 ? "warning" : "danger"))}
|
|
||||||
onClick={() => navigate("" + member.id)}>{rowContent}</div>
|
|
||||||
} else {
|
|
||||||
return <div className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
|
|
||||||
onClick={() => navigate("" + member.id)}>
|
|
||||||
{rowContent}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let allClub = []
|
|
||||||
|
|
||||||
function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source}) {
|
|
||||||
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>
|
|
||||||
<div className="mb-3">
|
|
||||||
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
|
|
||||||
</div>
|
|
||||||
{source !== "club" &&
|
|
||||||
<div className="mb-3">
|
|
||||||
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
|
||||||
<option value="">--- tout les clubs ---</option>
|
|
||||||
{allClub && allClub.map((value, index) => {
|
|
||||||
return <option key={index} value={value}>{value}</option>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import {Outlet} from "react-router-dom";
|
import {Outlet} from "react-router-dom";
|
||||||
import './AdminRoot.css'
|
import './AdminRoot.css'
|
||||||
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||||
import {MemberList} from "../MemberList.jsx";
|
import {MemberList} from "./MemberList.jsx";
|
||||||
import {MemberPage} from "./member/MemberPage.jsx";
|
import {MemberPage} from "./MemberPage.jsx";
|
||||||
import {NewMemberPage} from "./member/NewMemberPage.jsx";
|
|
||||||
|
|
||||||
export function AdminRoot() {
|
export function AdminRoot() {
|
||||||
return <>
|
return <>
|
||||||
@@ -14,20 +13,16 @@ export function AdminRoot() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAdminChildren() {
|
export function getAdminChildren () {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: 'member',
|
path: 'member',
|
||||||
element: <MemberList source="admin"/>
|
element: <MemberList/>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'member/:id',
|
path: 'member/:id',
|
||||||
element: <MemberPage/>
|
element: <MemberPage/>
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'member/new',
|
|
||||||
element: <NewMemberPage/>
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'b',
|
path: 'b',
|
||||||
element: <div>Admin B</div>
|
element: <div>Admin B</div>
|
||||||
|
|||||||
60
src/main/webapp/src/pages/admin/MemberList.jsx
Normal file
60
src/main/webapp/src/pages/admin/MemberList.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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 {useState} from "react";
|
||||||
|
import {Input} from "../../components/Input.jsx";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
const removeDiacritics = str => {
|
||||||
|
return str
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemberList() {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/member/all`, setLoading, 1)
|
||||||
|
const [searchInput, setSearchInput] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const visibleMember = data ? data.filter(member => {
|
||||||
|
const lo = removeDiacritics(searchInput).toLowerCase()
|
||||||
|
return !searchInput
|
||||||
|
|| (removeDiacritics(member.fname).toLowerCase().startsWith(lo)
|
||||||
|
|| removeDiacritics(member.lname).toLowerCase().startsWith(lo));
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<SearchBar searchInput={searchInput} onSearchInputChange={setSearchInput}/>
|
||||||
|
{data
|
||||||
|
? <div className="list-group">
|
||||||
|
{visibleMember.map(member => (
|
||||||
|
<span key={member.id}
|
||||||
|
onClick={() => navigate("/admin/member/" + member.id)}
|
||||||
|
className="list-group-item list-group-item-action">{member.fname} {member.lname}</span>))}
|
||||||
|
</div>
|
||||||
|
: error
|
||||||
|
? <AxiosError error={error}/>
|
||||||
|
: <Def/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBar({searchInput, onSearchInputChange}) {
|
||||||
|
return <div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<Input value={searchInput} onChange={onSearchInputChange} placeholder="Rechercher..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
411
src/main/webapp/src/pages/admin/MemberPage.jsx
Normal file
411
src/main/webapp/src/pages/admin/MemberPage.jsx
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import {useNavigate, useParams} from "react-router-dom";
|
||||||
|
import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||||
|
import {useFetch} from "../../hooks/useFetch.js";
|
||||||
|
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||||
|
import {ClubSelect} from "../../components/ClubSelect.jsx";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {apiAxios, getCategoryFormBirthDate} from "../../utils/Tools.js";
|
||||||
|
import imageCompression from "browser-image-compression";
|
||||||
|
import {ColoredCircle} from "../../components/ColoredCircle.jsx";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
|
const vite_url = import.meta.env.VITE_URL;
|
||||||
|
|
||||||
|
export function MemberPage() {
|
||||||
|
const {id} = useParams()
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<h2>Page membre</h2>
|
||||||
|
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||||
|
<< retour
|
||||||
|
</button>
|
||||||
|
{data
|
||||||
|
? <div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-4">
|
||||||
|
<PhotoCard data={data}/>
|
||||||
|
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-8">
|
||||||
|
<InformationForm data={data}/>
|
||||||
|
<LoadingProvider><PremForm userData={data}/></LoadingProvider>
|
||||||
|
<div className="row">
|
||||||
|
<LicenceCard/>
|
||||||
|
<SelectCard/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: error && <AxiosError error={error}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PhotoCard({data}) {
|
||||||
|
return <div className="card mb-4">
|
||||||
|
<div className="card-header">Licence n°{data.licence}</div>
|
||||||
|
<div className="card-body text-center">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<img
|
||||||
|
src={`${vite_url}/api/member/${data.id}/photo`}
|
||||||
|
alt="avatar"
|
||||||
|
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InformationForm({data}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const handleSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(1)
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("id", data.id);
|
||||||
|
formData.append("lname", event.target.lname?.value);
|
||||||
|
formData.append("fname", event.target.fname?.value);
|
||||||
|
formData.append("categorie", event.target.category?.value);
|
||||||
|
formData.append("club", event.target.club?.value);
|
||||||
|
formData.append("genre", event.target.genre?.value);
|
||||||
|
formData.append("country", event.target.country?.value);
|
||||||
|
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
||||||
|
formData.append("email", event.target.email?.value);
|
||||||
|
formData.append("role", event.target.role?.value);
|
||||||
|
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||||
|
|
||||||
|
const send = (formData_) => {
|
||||||
|
apiAxios.post(`/member/${data.id}`, formData_, {
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}
|
||||||
|
}).then(_ => {
|
||||||
|
toast.success('Profile mis à jours avec succès 🎉');
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e.response)
|
||||||
|
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
|
||||||
|
}).finally(() => {
|
||||||
|
if (setLoading)
|
||||||
|
setLoading(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageFile = event.target.url_photo.files[0];
|
||||||
|
if (imageFile) {
|
||||||
|
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
maxSizeMB: 1,
|
||||||
|
maxWidthOrHeight: 1920,
|
||||||
|
useWebWorker: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCompression(imageFile, options).then(compressedFile => {
|
||||||
|
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
||||||
|
formData.append("photo_data", compressedFile)
|
||||||
|
send(formData)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
send(formData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={handleSubmit}>
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">Information</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<TextField name="lname" text="Nom" value={data.lname}/>
|
||||||
|
<TextField name="fname" text="Prénom" value={data.fname}/>
|
||||||
|
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
||||||
|
type="email"/>
|
||||||
|
<OptionField name="genre" text="Genre" value={data.genre}
|
||||||
|
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||||
|
<OptionField name="country" text="Pays" value={data.country}
|
||||||
|
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
||||||
|
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
||||||
|
inti_category={data.categorie}/>
|
||||||
|
<div className="row">
|
||||||
|
<ClubSelect defaultValue={data?.club?.id} name="club"/>
|
||||||
|
</div>
|
||||||
|
<OptionField name="role" text="Rôle" value={data.role}
|
||||||
|
values={{
|
||||||
|
MEMBRE: 'Membre',
|
||||||
|
PRESIDENT: 'Président',
|
||||||
|
TRESORIER: 'Trésorier',
|
||||||
|
SECRETAIRE: 'Secrétaire'
|
||||||
|
}}/>
|
||||||
|
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||||
|
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||||
|
(optionnelle)</label>
|
||||||
|
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
||||||
|
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-12 text-right">
|
||||||
|
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PremForm({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const handleSubmitPerm = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(1)
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("federation_admin", event.target.federation_admin?.checked);
|
||||||
|
formData.append("safca_user", event.target.safca_user?.checked);
|
||||||
|
formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
|
||||||
|
formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
|
||||||
|
|
||||||
|
apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Content-Type': 'form-data',
|
||||||
|
}
|
||||||
|
}).then(_ => {
|
||||||
|
toast.success('Permission mise à jours avec succès 🎉');
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e.response)
|
||||||
|
toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
|
||||||
|
}).finally(() => {
|
||||||
|
if (setLoading)
|
||||||
|
setLoading(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={handleSubmitPerm}>
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">Permission</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row g-3">
|
||||||
|
{userData.userId
|
||||||
|
? <PremFormContent userData={userData}/>
|
||||||
|
: <div className="col">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Ce membre ne dispose pas de compte...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-12 text-right">
|
||||||
|
{userData.userId && <button type="submit" className="btn btn-primary">Enregistrer</button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PremFormContent({userData}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="col">
|
||||||
|
<h5>FFSAF intra</h5>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<CheckField name="federation_admin" text="Accès à l'intra"
|
||||||
|
value={data.includes("federation_admin")}/>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>}
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
<h5>SAFCA</h5>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<CheckField name="safca_user" text="Accès à l'application" value={data.includes("safca_user")}/>
|
||||||
|
<CheckField name="safca_create_compet" text="Créer des compétion"
|
||||||
|
value={data.includes("safca_create_compet")}/>
|
||||||
|
<CheckField name="safca_super_admin" text="Super administrateur"
|
||||||
|
value={data.includes("safca_super_admin")}/>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function LicenceCard() {
|
||||||
|
return <div className="col-md-6">
|
||||||
|
<div className="card mb-4 mb-md-0">
|
||||||
|
<div className="card-header">Licence</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p className="mb-1">Web Design</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectCard() {
|
||||||
|
return <div className="col-md-6">
|
||||||
|
<div className="card mb-4 mb-md-0">
|
||||||
|
<div className="card-header">Sélection en équipe de France</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p className="mb-1">Web Design</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompteInfo({userData}) {
|
||||||
|
|
||||||
|
const creatAccount = () => {
|
||||||
|
let err = {};
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
|
||||||
|
err = e
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
pending: 'Création du compte en cours',
|
||||||
|
success: 'Compte créé avec succès 🎉',
|
||||||
|
error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="card mb-4">
|
||||||
|
<div className="card-header">Compte</div>
|
||||||
|
<div className="card-body text-center">
|
||||||
|
{userData.userId
|
||||||
|
? <CompteInfoContent userData={userData}/>
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Ce membre ne dispose pas de compte...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompteInfoContent({
|
||||||
|
userData
|
||||||
|
}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{data
|
||||||
|
? <>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Identifiant: {data.login}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
: error && <AxiosError error={error}/>
|
||||||
|
} </>
|
||||||
|
}
|
||||||
|
|
||||||
|
function BirthDayField({inti_date, inti_category}) {
|
||||||
|
const [date, setDate] = useState(inti_date)
|
||||||
|
const [category, setCategory] = useState(inti_category)
|
||||||
|
const [canUpdate, setCanUpdate] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
|
||||||
|
if (b !== canUpdate)
|
||||||
|
setCanUpdate(b)
|
||||||
|
}, [date, category])
|
||||||
|
|
||||||
|
const updateCat = _ => {
|
||||||
|
setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<span className="input-group-text" id="birth_date">Date de naissance</span>
|
||||||
|
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
|
||||||
|
name="birth_date" aria-describedby="birth_date" defaultValue={date} required
|
||||||
|
onChange={(e) => setDate(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<span className="input-group-text" id="category">Catégorie</span>
|
||||||
|
<input type="text" className="form-control" placeholder="" name="category"
|
||||||
|
aria-label="category" value={category} aria-describedby="category"
|
||||||
|
disabled/>
|
||||||
|
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
||||||
|
onClick={updateCat}>Mettre à jours</button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function OptionField({name, text, values, value}) {
|
||||||
|
return <div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<label className="input-group-text" id={name}>{text}</label>
|
||||||
|
<select className="form-select" id={name} name={name} defaultValue={value} required>
|
||||||
|
{Object.keys(values).map((key, _) => {
|
||||||
|
return (<option key={key} value={key}>{values[key]}</option>)
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextField({name, text, value, placeholder, type = "text"}) {
|
||||||
|
return <div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<span className="input-group-text" id={name}>{text}</span>
|
||||||
|
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
|
||||||
|
name={name} aria-describedby={name} defaultValue={value} required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckField({name, text, value, row = false}) {
|
||||||
|
return <>{
|
||||||
|
row ?
|
||||||
|
<div className="row">
|
||||||
|
<div className="input-group mb-3">
|
||||||
|
<div className="form-check">
|
||||||
|
<input className="form-check-input" type="checkbox" id={name} name={name}
|
||||||
|
defaultChecked={value}/>
|
||||||
|
<label className="form-check-label" htmlFor={name}>{text}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <div className="form-check">
|
||||||
|
<input className="form-check-input" type="checkbox" id={name} name={name} defaultChecked={value}/>
|
||||||
|
<label className="form-check-label" htmlFor={name}>{text}</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import {toast} from "react-toastify";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
|
||||||
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
|
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
|
|
||||||
export function CompteInfo({userData}) {
|
|
||||||
|
|
||||||
const creatAccount = () => {
|
|
||||||
let err = {};
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
|
|
||||||
err = e
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
pending: 'Création du compte en cours',
|
|
||||||
success: 'Compte créé avec succès 🎉',
|
|
||||||
error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const sendId = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`),
|
|
||||||
{
|
|
||||||
pending: "Définition de l'identifient en cours",
|
|
||||||
success: "Identifient défini avec succès 🎉",
|
|
||||||
error: "Échec de la définition de l'identifient 😕 "
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="card mb-4">
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="btn-group dropend">
|
|
||||||
<div className="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
Compte
|
|
||||||
</div>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<button type="button" className="btn btn-primary" data-bs-toggle="modal"
|
|
||||||
data-bs-target="#comptIdModal">Définir l'id du compte
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body text-center">
|
|
||||||
{userData.userId
|
|
||||||
? <CompteInfoContent userData={userData}/>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Ce membre ne dispose pas de compte...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="modal fade" id="comptIdModal" tabIndex="-1" aria-labelledby="comptIdModalLabel"
|
|
||||||
aria-hidden="true">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<form onSubmit={sendId}>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h1 className="modal-title fs-5" id="comptIdModalLabel">Entré l'UUID du compte</h1>
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<h5>Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...</h5>
|
|
||||||
<input type="text" className="form-control" placeholder="uuid" name="uuid"
|
|
||||||
defaultValue={userData.userId}/>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Appliquer</button>
|
|
||||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function CompteInfoContent({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Identifiant: {data.login}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>
|
|
||||||
} </>
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import imageCompression from "browser-image-compression";
|
|
||||||
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
|
||||||
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
|
||||||
|
|
||||||
export function addPhoto(event, formData, send) {
|
|
||||||
const imageFile = event.target.url_photo.files[0];
|
|
||||||
if (imageFile) {
|
|
||||||
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
maxSizeMB: 1,
|
|
||||||
maxWidthOrHeight: 1920,
|
|
||||||
useWebWorker: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
imageCompression(imageFile, options).then(compressedFile => {
|
|
||||||
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
|
||||||
formData.append("photo_data", compressedFile)
|
|
||||||
send(formData)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
send(formData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InformationForm({data}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const handleSubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("id", data.id);
|
|
||||||
formData.append("lname", event.target.lname?.value.toUpperCase());
|
|
||||||
formData.append("fname", event.target.fname?.value);
|
|
||||||
formData.append("categorie", event.target.category?.value);
|
|
||||||
formData.append("club", event.target.club?.value);
|
|
||||||
formData.append("genre", event.target.genre?.value);
|
|
||||||
formData.append("country", event.target.country?.value);
|
|
||||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
|
||||||
formData.append("email", event.target.email?.value);
|
|
||||||
formData.append("role", event.target.role?.value);
|
|
||||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
|
||||||
|
|
||||||
const send = (formData_) => {
|
|
||||||
apiAxios.put(`/member/${data.id}`, formData_, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
}
|
|
||||||
}).then(_ => {
|
|
||||||
toast.success('Profile mis à jours avec succès 🎉');
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
addPhoto(event, formData, send);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmit}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Information</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<TextField name="lname" text="Nom" value={data.lname}/>
|
|
||||||
<TextField name="fname" text="Prénom" value={data.fname}/>
|
|
||||||
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
|
||||||
type="email"/>
|
|
||||||
<OptionField name="genre" text="Genre" value={data.genre}
|
|
||||||
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
|
||||||
<OptionField name="country" text="Pays" value={data.country}
|
|
||||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
|
||||||
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
|
||||||
inti_category={data.categorie}/>
|
|
||||||
<div className="row">
|
|
||||||
<ClubSelect defaultValue={data?.club?.id} name="club"/>
|
|
||||||
</div>
|
|
||||||
<OptionField name="role" text="Rôle" value={data.role}
|
|
||||||
values={{
|
|
||||||
MEMBRE: 'Membre',
|
|
||||||
PRESIDENT: 'Président',
|
|
||||||
TRESORIER: 'Trésorier',
|
|
||||||
SECRETAIRE: 'Secrétaire'
|
|
||||||
}}/>
|
|
||||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
|
||||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
|
||||||
(optionnelle)</label>
|
|
||||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
|
||||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
||||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>;
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
|
||||||
import {useEffect, useReducer, useState} from "react";
|
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|
||||||
import {faPen} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
|
||||||
import {apiAxios, getSaison} from "../../../utils/Tools.js";
|
|
||||||
import {Input} from "../../../components/Input.jsx";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
|
|
||||||
function licenceReducer(licences, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'ADD':
|
|
||||||
return [
|
|
||||||
...licences,
|
|
||||||
action.payload
|
|
||||||
]
|
|
||||||
case 'REMOVE':
|
|
||||||
return licences.filter(licence => licence.id !== action.payload)
|
|
||||||
case 'UPDATE_OR_ADD':
|
|
||||||
const index = licences.findIndex(licence => licence.id === action.payload.id)
|
|
||||||
if (index === -1) {
|
|
||||||
return [
|
|
||||||
...licences,
|
|
||||||
action.payload
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
licences[index] = action.payload
|
|
||||||
return [...licences]
|
|
||||||
}
|
|
||||||
case 'SORT':
|
|
||||||
return licences.sort((a, b) => b.saison - a.saison)
|
|
||||||
default:
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LicenceCard({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
|
|
||||||
|
|
||||||
const [modalLicence, setModal] = useState({id: -1, membre: userData.id})
|
|
||||||
const [licences, dispatch] = useReducer(licenceReducer, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return
|
|
||||||
for (const dataKey of data) {
|
|
||||||
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
|
|
||||||
}
|
|
||||||
dispatch({type: 'SORT'})
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return <div className="card mb-4 mb-md-0">
|
|
||||||
<div className="card-header container-fluid">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col">Licence</div>
|
|
||||||
<div className="col" style={{textAlign: 'right'}}>
|
|
||||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
|
||||||
onClick={_ => setModal({id: -1, membre: userData.id})}>Ajouter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="list-group">
|
|
||||||
{licences.map((licence, index) => {
|
|
||||||
return <div key={index}
|
|
||||||
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
|
|
||||||
(licence.validate ? "success" : (licence.certificate ? "warning" : "danger"))}>
|
|
||||||
<div className="me-auto">{licence?.saison}-{licence?.saison + 1}</div>
|
|
||||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
|
||||||
data-bs-target="#LicenceModal" onClick={_ => setModal(licence)}>
|
|
||||||
<FontAwesomeIcon icon={faPen}/></button>
|
|
||||||
</div>
|
|
||||||
})}
|
|
||||||
{error && <AxiosError error={error}/>}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal fade" id="LicenceModal" tabIndex="-1" aria-labelledby="LicenceModalLabel"
|
|
||||||
aria-hidden="true">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<ModalContent licence={modalLicence} dispatch={dispatch}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendLicence(event, dispatch) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const formData = new FormData(event.target);
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.post(`/licence/${formData.get('membre')}`, formData),
|
|
||||||
{
|
|
||||||
pending: "Enregistrement de la licence en cours",
|
|
||||||
success: "Licence enregistrée avec succès 🎉",
|
|
||||||
error: "Échec de l'enregistrement de la licence 😕"
|
|
||||||
}
|
|
||||||
).then(data => {
|
|
||||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
|
||||||
dispatch({type: 'SORT'})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLicence(id, dispatch) {
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.delete(`/licence/${id}`),
|
|
||||||
{
|
|
||||||
pending: "Suppression de la licence en cours",
|
|
||||||
success: "Licence supprimée avec succès 🎉",
|
|
||||||
error: "Échec de la suppression de la licence 😕"
|
|
||||||
}
|
|
||||||
).then(_ => {
|
|
||||||
dispatch({type: 'REMOVE', payload: id})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModalContent({licence, dispatch}) {
|
|
||||||
const [saison, setSaison] = useState(0)
|
|
||||||
const [certificate, setCertificate] = useState(false)
|
|
||||||
const [validate, setValidate] = useState(false)
|
|
||||||
const [isNew, setNew] = useState(true)
|
|
||||||
const setSeason = (event) => {
|
|
||||||
setSaison(Number(event.target.value))
|
|
||||||
}
|
|
||||||
const handleCertificateChange = (event) => {
|
|
||||||
setCertificate(event.target.value === 'true');
|
|
||||||
}
|
|
||||||
const handleValidateChange = (event) => {
|
|
||||||
setValidate(event.target.value === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (licence.id !== -1) {
|
|
||||||
setNew(false)
|
|
||||||
setSaison(licence.saison)
|
|
||||||
setCertificate(licence.certificate)
|
|
||||||
setValidate(licence.validate)
|
|
||||||
} else {
|
|
||||||
setNew(true)
|
|
||||||
setSaison(getSaison())
|
|
||||||
setCertificate(false)
|
|
||||||
setValidate(false)
|
|
||||||
}
|
|
||||||
}, [licence]);
|
|
||||||
|
|
||||||
return <form onSubmit={e => sendLicence(e, dispatch)}>
|
|
||||||
<input name="id" value={licence.id} readOnly hidden/>
|
|
||||||
<input name="membre" value={licence.membre} readOnly hidden/>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h1 className="modal-title fs-5" id="LicenceModalLabel">Edition de la licence</h1>
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<div className="input-group mb-3 justify-content-md-center">
|
|
||||||
{isNew
|
|
||||||
? <input type="number" className="form-control" placeholder="Saison" name="saison"
|
|
||||||
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
|
|
||||||
: <><span className="input-group-text" id="basic-addon2">{saison}</span>
|
|
||||||
<input name="saison" value={saison} readOnly hidden/></>}
|
|
||||||
<span className="input-group-text" id="basic-addon2">-</span>
|
|
||||||
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
|
|
||||||
</div>
|
|
||||||
<RadioGroupeOnOff name="certificate" text="Certificat médical valide" value={certificate}
|
|
||||||
onChange={handleCertificateChange}/>
|
|
||||||
<RadioGroupeOnOff name="validate" text="Validation de la licence" value={validate}
|
|
||||||
onChange={handleValidateChange}/>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
|
|
||||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
|
||||||
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
|
||||||
onClick={() => removeLicence(licence.id, dispatch)}>Supprimer</button>}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioGroupeOnOff({value, onChange, name, text}) {
|
|
||||||
return <div className="btn-group input-group mb-3 justify-content-md-center" role="group"
|
|
||||||
aria-label="Basic radio toggle button group">
|
|
||||||
<span className="input-group-text">{text}</span>
|
|
||||||
<input type="radio" className="btn-check" id={"btnradio1" + name} autoComplete="off"
|
|
||||||
value="false" checked={value === false} onChange={onChange}/>
|
|
||||||
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + name}>Non</label>
|
|
||||||
<input type="radio" className="btn-check" name={name} id={"btnradio2" + name} autoComplete="off"
|
|
||||||
value="true" checked={value === true} onChange={onChange}/>
|
|
||||||
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + name}>Oui</label>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import {useNavigate, useParams} from "react-router-dom";
|
|
||||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
import {CompteInfo} from "./CompteInfo.jsx";
|
|
||||||
import {PremForm} from "./PremForm.jsx";
|
|
||||||
import {InformationForm} from "./InformationForm.jsx";
|
|
||||||
import {LicenceCard} from "./LicenceCard.jsx";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
|
||||||
|
|
||||||
const vite_url = import.meta.env.VITE_URL;
|
|
||||||
|
|
||||||
export function MemberPage() {
|
|
||||||
const {id} = useParams()
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
|
||||||
|
|
||||||
const handleRm = () => {
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.delete(`/member/${id}`),
|
|
||||||
{
|
|
||||||
pending: "Suppression du compte en cours...",
|
|
||||||
success: "Compte supprimé avec succès 🎉",
|
|
||||||
error: "Échec de la suppression du compte 😕"
|
|
||||||
}
|
|
||||||
).then(_ => {
|
|
||||||
navigate("/admin/member")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<h2>Page membre</h2>
|
|
||||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
|
||||||
« retour
|
|
||||||
</button>
|
|
||||||
{data
|
|
||||||
? <div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-lg-4">
|
|
||||||
<PhotoCard data={data}/>
|
|
||||||
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-8">
|
|
||||||
<InformationForm data={data}/>
|
|
||||||
<LoadingProvider><PremForm userData={data}/></LoadingProvider>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
|
||||||
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
|
||||||
onConfirm={handleRm}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: error && <AxiosError error={error}/>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function PhotoCard({data}) {
|
|
||||||
return <div className="card mb-4">
|
|
||||||
<div className="card-header">Licence n°{data.licence}</div>
|
|
||||||
<div className="card-body text-center">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<img
|
|
||||||
src={`${vite_url}/api/member/${data.id}/photo`}
|
|
||||||
alt="avatar"
|
|
||||||
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectCard() {
|
|
||||||
return <div className="card mb-4 mb-md-0">
|
|
||||||
<div className="card-header">Sélection en équipe de France</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<p className="mb-1">Web Design</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import {useNavigate} from "react-router-dom";
|
|
||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
|
||||||
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
|
||||||
import {addPhoto} from "./InformationForm.jsx";
|
|
||||||
|
|
||||||
export function NewMemberPage() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<h2>Page membre</h2>
|
|
||||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
|
||||||
« retour
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<div className="row">
|
|
||||||
<Form/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Form() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
|
|
||||||
const handleSubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("id", -1);
|
|
||||||
formData.append("lname", event.target.lname?.value.toUpperCase());
|
|
||||||
formData.append("fname", event.target.fname?.value);
|
|
||||||
formData.append("categorie", event.target.category?.value);
|
|
||||||
formData.append("club", event.target.club?.value);
|
|
||||||
formData.append("genre", event.target.genre?.value);
|
|
||||||
formData.append("country", event.target.country?.value);
|
|
||||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
|
||||||
formData.append("email", event.target.email?.value);
|
|
||||||
formData.append("role", event.target.role?.value);
|
|
||||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
|
||||||
|
|
||||||
const send = (formData_) => {
|
|
||||||
apiAxios.post(`/member`, formData_, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
}
|
|
||||||
}).then(data => {
|
|
||||||
toast.success('Profile crée avec succès 🎉');
|
|
||||||
navigate(`/admin/member/${data.data}`)
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addPhoto(event, formData, send);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmit}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Nouveau membre</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<TextField name="lname" text="Nom"/>
|
|
||||||
<TextField name="fname" text="Prénom"/>
|
|
||||||
<TextField name="email" text="Email" placeholder="name@example.com"
|
|
||||||
type="email"/>
|
|
||||||
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
|
||||||
<OptionField name="country" text="Pays" value={'fr'}
|
|
||||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
|
||||||
<BirthDayField/>
|
|
||||||
<div className="row">
|
|
||||||
<ClubSelect name="club"/>
|
|
||||||
</div>
|
|
||||||
<OptionField name="role" text="Rôle" value={'MEMBRE'}
|
|
||||||
values={{
|
|
||||||
MEMBRE: 'Membre',
|
|
||||||
PRESIDENT: 'Président',
|
|
||||||
TRESORIER: 'Trésorier',
|
|
||||||
SECRETAIRE: 'Secrétaire'
|
|
||||||
}}/>
|
|
||||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={'NA'}
|
|
||||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
|
||||||
(optionnelle)</label>
|
|
||||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
|
||||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
||||||
<button type="submit" className="btn btn-primary">Créer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
|
||||||
import {CheckField} from "../../../components/MemberCustomFiels.jsx";
|
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
|
|
||||||
export function PremForm({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const handleSubmitPerm = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("federation_admin", event.target.federation_admin?.checked);
|
|
||||||
formData.append("safca_user", event.target.safca_user?.checked);
|
|
||||||
formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
|
|
||||||
formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
|
|
||||||
|
|
||||||
apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'form-data',
|
|
||||||
}
|
|
||||||
}).then(_ => {
|
|
||||||
toast.success('Permission mise à jours avec succès 🎉');
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmitPerm}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Permission</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="row g-3">
|
|
||||||
{userData.userId
|
|
||||||
? <PremFormContent userData={userData}/>
|
|
||||||
: <div className="col">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Ce membre ne dispose pas de compte...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
||||||
{userData.userId && <button type="submit" className="btn btn-primary">Enregistrer</button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
function PremFormContent({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="col">
|
|
||||||
<h5>FFSAF intra</h5>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<CheckField name="federation_admin" text="Administrateur de la fédération"
|
|
||||||
value={data.includes("federation_admin")}/>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>}
|
|
||||||
</div>
|
|
||||||
<div className="col">
|
|
||||||
<h5>SAFCA</h5>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<CheckField name="safca_user" text="Accès à l'application" value={data.includes("safca_user")}/>
|
|
||||||
<CheckField name="safca_create_compet" text="Créer des compétion"
|
|
||||||
value={data.includes("safca_create_compet")}/>
|
|
||||||
<CheckField name="safca_super_admin" text="Super administrateur"
|
|
||||||
value={data.includes("safca_super_admin")}/>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import {Outlet} from "react-router-dom";
|
|
||||||
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
|
||||||
import {MemberPage} from "./member/MemberPage.jsx";
|
|
||||||
import {useAuth} from "../../hooks/useAuth.jsx";
|
|
||||||
import {MemberList} from "../MemberList.jsx";
|
|
||||||
import {NewMemberPage} from "./member/NewMemberPage.jsx";
|
|
||||||
|
|
||||||
export function ClubRoot() {
|
|
||||||
const {userinfo} = useAuth()
|
|
||||||
let club = ""
|
|
||||||
if (userinfo?.groups) {
|
|
||||||
for (let group of userinfo.groups) {
|
|
||||||
if (group.startsWith("/club/")) {
|
|
||||||
club = group.slice(group.indexOf("-") + 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}>
|
|
||||||
<h1>Espace club</h1><h3 style={{marginLeft: '0.75em'}}>{club}</h3></div>
|
|
||||||
<LoadingProvider>
|
|
||||||
<Outlet/>
|
|
||||||
</LoadingProvider>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getClubChildren() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
path: 'member',
|
|
||||||
element: <MemberList source="club"/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'member/:id',
|
|
||||||
element: <MemberPage/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'member/new',
|
|
||||||
element: <NewMemberPage/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'b',
|
|
||||||
element: <div>Club B</div>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import {toast} from "react-toastify";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
|
||||||
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
|
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
|
|
||||||
export function CompteInfo({userData}) {
|
|
||||||
|
|
||||||
return <div className="card mb-4">
|
|
||||||
<div className="card-header">Compte</div>
|
|
||||||
<div className="card-body text-center">
|
|
||||||
{userData.userId
|
|
||||||
? <CompteInfoContent userData={userData}/>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Ce membre ne dispose pas de compte... <br/>
|
|
||||||
Un compte sera créé par la fédération lors de la validation de sa première licence
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function CompteInfoContent({userData}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
{data
|
|
||||||
? <>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Identifiant: {data.login}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
: error && <AxiosError error={error}/>
|
|
||||||
} </>
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
// noinspection DuplicatedCode
|
|
||||||
|
|
||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
|
||||||
import {addPhoto} from "../../admin/member/InformationForm.jsx";
|
|
||||||
|
|
||||||
export function InformationForm({data}) {
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const handleSubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("id", data.id);
|
|
||||||
formData.append("lname", event.target.lname?.value.toUpperCase());
|
|
||||||
formData.append("fname", event.target.fname?.value);
|
|
||||||
formData.append("categorie", event.target.category?.value);
|
|
||||||
formData.append("genre", event.target.genre?.value);
|
|
||||||
formData.append("country", event.target.country?.value);
|
|
||||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
|
||||||
formData.append("email", event.target.email?.value);
|
|
||||||
formData.append("role", event.target.role?.value);
|
|
||||||
|
|
||||||
const send = (formData_) => {
|
|
||||||
apiAxios.put(`/member/club/${data.id}`, formData_, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
}
|
|
||||||
}).then(_ => {
|
|
||||||
toast.success('Profile mis à jours avec succès 🎉');
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
addPhoto(event, formData, send);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmit}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Information</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<TextField name="lname" text="Nom" value={data.lname}/>
|
|
||||||
<TextField name="fname" text="Prénom" value={data.fname}/>
|
|
||||||
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
|
||||||
type="email"/>
|
|
||||||
<OptionField name="genre" text="Genre" value={data.genre}
|
|
||||||
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
|
||||||
<OptionField name="country" text="Pays" value={data.country}
|
|
||||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
|
||||||
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
|
||||||
inti_category={data.categorie}/>
|
|
||||||
<OptionField name="role" text="Rôle" value={data.role}
|
|
||||||
values={{
|
|
||||||
MEMBRE: 'Membre',
|
|
||||||
PRESIDENT: 'Président',
|
|
||||||
TRESORIER: 'Trésorier',
|
|
||||||
SECRETAIRE: 'Secrétaire'
|
|
||||||
}} disabled={true}/>
|
|
||||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
|
||||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
|
||||||
(optionnelle)</label>
|
|
||||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
|
||||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
||||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>;
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
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 {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
import {apiAxios, getSaison} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {ColoredText} from "../../../components/ColoredCircle.jsx";
|
|
||||||
|
|
||||||
function licenceReducer(licences, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'REMOVE':
|
|
||||||
return licences.filter(licence => licence.id !== action.payload)
|
|
||||||
case 'UPDATE_OR_ADD':
|
|
||||||
const index = licences.findIndex(licence => licence.id === action.payload.id)
|
|
||||||
if (index === -1) {
|
|
||||||
return [
|
|
||||||
...licences,
|
|
||||||
action.payload
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
licences[index] = action.payload
|
|
||||||
return [...licences]
|
|
||||||
}
|
|
||||||
case 'SORT':
|
|
||||||
return licences.sort((a, b) => b.saison - a.saison)
|
|
||||||
default:
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LicenceCard({userData}) {
|
|
||||||
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false}
|
|
||||||
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
|
|
||||||
const [modalLicence, setModal] = useState(defaultLicence)
|
|
||||||
const [licences, dispatch] = useReducer(licenceReducer, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return
|
|
||||||
for (const dataKey of data) {
|
|
||||||
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
|
|
||||||
}
|
|
||||||
dispatch({type: 'SORT'})
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return <div className="card mb-4 mb-md-0">
|
|
||||||
<div className="card-header container-fluid">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col">Licence</div>
|
|
||||||
<div className="col" style={{textAlign: 'right'}}>
|
|
||||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
|
||||||
onClick={() => setModal(defaultLicence)}
|
|
||||||
disabled={licences.some(licence => licence.saison === getSaison())}>Demander
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="list-group">
|
|
||||||
{licences.map((licence, index) => {
|
|
||||||
return <div key={index}
|
|
||||||
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
|
|
||||||
(licence.validate ? "success" : (licence.certificate ? "warning" : "danger"))}>
|
|
||||||
<div className="me-auto">{licence?.saison}-{licence?.saison + 1}</div>
|
|
||||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
|
||||||
data-bs-target="#LicenceModal" onClick={_ => setModal(licence)}>
|
|
||||||
{licence.saison === getSaison() ? <FontAwesomeIcon icon={faPen}/> :
|
|
||||||
<FontAwesomeIcon icon={faInfo}/>}</button>
|
|
||||||
</div>
|
|
||||||
})}
|
|
||||||
{error && <AxiosError error={error}/>}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal fade" id="LicenceModal" tabIndex="-1" aria-labelledby="LicenceModalLabel"
|
|
||||||
aria-hidden="true">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<ModalContent licence={modalLicence} dispatch={dispatch}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendLicence(event, dispatch) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const formData = new FormData(event.target);
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.post(`/licence/club/${formData.get('membre')}`, formData),
|
|
||||||
{
|
|
||||||
pending: "Enregistrement de la demande de licence en cours",
|
|
||||||
success: "Demande de licence enregistrée avec succès 🎉",
|
|
||||||
error: "Échec de la demande de licence 😕"
|
|
||||||
}
|
|
||||||
).then(data => {
|
|
||||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
|
||||||
dispatch({type: 'SORT'})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLicence(id, dispatch) {
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.delete(`/licence/club/${id}`),
|
|
||||||
{
|
|
||||||
pending: "Suppression de la demande en cours",
|
|
||||||
success: "Demande supprimée avec succès 🎉",
|
|
||||||
error: "Échec de la suppression de la demande de licence 😕"
|
|
||||||
}
|
|
||||||
).then(_ => {
|
|
||||||
dispatch({type: 'REMOVE', payload: id})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModalContent({licence, dispatch}) {
|
|
||||||
const [certificate, setCertificate] = useState(false)
|
|
||||||
const [isNew, setNew] = useState(true)
|
|
||||||
|
|
||||||
const handleCertificateChange = (event) => {
|
|
||||||
setCertificate(event.target.value === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (licence.id !== -1) {
|
|
||||||
setNew(false)
|
|
||||||
setCertificate(licence.certificate)
|
|
||||||
} else {
|
|
||||||
setNew(true)
|
|
||||||
setCertificate(false)
|
|
||||||
}
|
|
||||||
}, [licence]);
|
|
||||||
|
|
||||||
const currentSaison = licence.saison === getSaison();
|
|
||||||
|
|
||||||
return <form onSubmit={e => sendLicence(e, dispatch)}>
|
|
||||||
<input name="id" value={licence.id} readOnly hidden/>
|
|
||||||
<input name="membre" value={licence.membre} readOnly hidden/>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h1 className="modal-title fs-5" id="LicenceModalLabel">
|
|
||||||
{isNew ? "Demande de licence " : "Edition de la demande "}
|
|
||||||
(saison {licence.saison}-{licence.saison + 1})</h1>
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<div className="btn-group input-group mb-3 justify-content-md-center" role="group"
|
|
||||||
aria-label="Basic radio toggle button group">
|
|
||||||
<span className="input-group-text">Certificat médical</span>
|
|
||||||
<input type="radio" className="btn-check" id="btnradio1" autoComplete="off" value="false"
|
|
||||||
checked={certificate === false} onChange={handleCertificateChange} disabled={!currentSaison}/>
|
|
||||||
<label className="btn btn-outline-primary" htmlFor="btnradio1">Non</label>
|
|
||||||
<input type="radio" className="btn-check" name="certificate" id="btnradio2" autoComplete="off"
|
|
||||||
value="true" checked={certificate === true} onChange={handleCertificateChange}
|
|
||||||
disabled={!currentSaison}/>
|
|
||||||
<label className="btn btn-outline-primary" htmlFor="btnradio2">Oui</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group mb-3 justify-content-md-center">
|
|
||||||
<div>Validation de la licence: <ColoredText boolean={licence.validate}/></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
{currentSaison &&
|
|
||||||
<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 === false &&
|
|
||||||
<button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
|
||||||
onClick={() => removeLicence(licence.id, dispatch)}>Annuler</button>}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import {useNavigate, useParams} from "react-router-dom";
|
|
||||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {useFetch} from "../../../hooks/useFetch.js";
|
|
||||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
|
||||||
import {CompteInfo} from "./CompteInfo.jsx";
|
|
||||||
import {InformationForm} from "./InformationForm.jsx";
|
|
||||||
import {LicenceCard} from "./LicenceCard.jsx";
|
|
||||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
|
|
||||||
const vite_url = import.meta.env.VITE_URL;
|
|
||||||
|
|
||||||
export function MemberPage() {
|
|
||||||
const {id} = useParams()
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
|
|
||||||
|
|
||||||
const handleRm = () => {
|
|
||||||
toast.promise(
|
|
||||||
apiAxios.delete(`/member/club/${id}`),
|
|
||||||
{
|
|
||||||
pending: "Suppression du compte en cours...",
|
|
||||||
success: "Compte supprimé avec succès 🎉",
|
|
||||||
error: "Échec de la suppression du compte 😕"
|
|
||||||
}
|
|
||||||
).then(_ => {
|
|
||||||
navigate("/club/member")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<h2>Page membre</h2>
|
|
||||||
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
|
||||||
« retour
|
|
||||||
</button>
|
|
||||||
{data
|
|
||||||
? <div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-lg-4">
|
|
||||||
<PhotoCard data={data}/>
|
|
||||||
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-8">
|
|
||||||
<InformationForm data={data}/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
|
||||||
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
|
||||||
onConfirm={handleRm}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: error && <AxiosError error={error}/>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function PhotoCard({data}) {
|
|
||||||
return <div className="card mb-4">
|
|
||||||
<div className="card-header">Licence n°{data.licence}</div>
|
|
||||||
<div className="card-body text-center">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<img
|
|
||||||
src={`${vite_url}/api/member/${data.id}/photo`}
|
|
||||||
alt="avatar"
|
|
||||||
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectCard() {
|
|
||||||
return <div className="card mb-4 mb-md-0">
|
|
||||||
<div className="card-header">Sélection en équipe de France</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<p className="mb-1">Soon</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import {useNavigate} from "react-router-dom";
|
|
||||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
||||||
import {apiAxios} from "../../../utils/Tools.js";
|
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
|
||||||
import {addPhoto} from "../../admin/member/InformationForm.jsx";
|
|
||||||
|
|
||||||
export function NewMemberPage() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<h2>Page membre</h2>
|
|
||||||
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
|
||||||
« retour
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<div className="row">
|
|
||||||
<Form/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Form() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const setLoading = useLoadingSwitcher()
|
|
||||||
|
|
||||||
const handleSubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(1)
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("id", -1);
|
|
||||||
formData.append("lname", event.target.lname?.value.toUpperCase());
|
|
||||||
formData.append("fname", event.target.fname?.value);
|
|
||||||
formData.append("categorie", event.target.category?.value);
|
|
||||||
formData.append("genre", event.target.genre?.value);
|
|
||||||
formData.append("country", event.target.country?.value);
|
|
||||||
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
|
|
||||||
formData.append("email", event.target.email?.value);
|
|
||||||
|
|
||||||
const send = (formData_) => {
|
|
||||||
apiAxios.post(`/member/club`, formData_, {
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
}
|
|
||||||
}).then(data => {
|
|
||||||
toast.success('Profile crée avec succès 🎉');
|
|
||||||
navigate(`/club/member/${data.data}`)
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e.response)
|
|
||||||
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
|
|
||||||
}).finally(() => {
|
|
||||||
if (setLoading)
|
|
||||||
setLoading(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addPhoto(event, formData, send);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleSubmit}>
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-header">Nouveau membre</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<TextField name="lname" text="Nom"/>
|
|
||||||
<TextField name="fname" text="Prénom"/>
|
|
||||||
<TextField name="email" text="Email" placeholder="name@example.com"
|
|
||||||
type="email"/>
|
|
||||||
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
|
||||||
<OptionField name="country" text="Pays" value={'fr'}
|
|
||||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
|
||||||
<BirthDayField/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="input-group mb-3">
|
|
||||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
|
||||||
(optionnelle)</label>
|
|
||||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
|
||||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
||||||
<button type="submit" className="btn btn-primary">Créer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user