i18n #99
48
src/main/java/fr/titionfire/ffsaf/UserInfoProvider.java
Normal file
48
src/main/java/fr/titionfire/ffsaf/UserInfoProvider.java
Normal file
@ -0,0 +1,48 @@
|
||||
package fr.titionfire.ffsaf;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.container.PreMatching;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Provider
|
||||
@PreMatching
|
||||
public class UserInfoProvider implements ContainerRequestFilter {
|
||||
|
||||
private static final List<Locale> SUPPORTED_LANGUAGES = Arrays.asList(
|
||||
Locale.FRENCH,
|
||||
Locale.ENGLISH
|
||||
);
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
List<Locale> acceptableLanguages = requestContext.getAcceptableLanguages();
|
||||
Locale selectedLocale = findFirstSupportedLanguage(acceptableLanguages);
|
||||
|
||||
if (selectedLocale == null)
|
||||
selectedLocale = TradService.fallbackLocale;
|
||||
requestContext.setProperty("userLocale", selectedLocale);
|
||||
}
|
||||
|
||||
private Locale findFirstSupportedLanguage(List<Locale> acceptableLanguages) {
|
||||
for (Locale acceptableLanguage : acceptableLanguages) {
|
||||
// Vérifie si la langue est dans la liste des langues supportées
|
||||
if (SUPPORTED_LANGUAGES.contains(acceptableLanguage)) {
|
||||
return acceptableLanguage;
|
||||
}
|
||||
// Vérifie aussi par tag de langue (ex: "fr-FR" -> "fr")
|
||||
String languageTag = acceptableLanguage.getLanguage();
|
||||
for (Locale supportedLanguage : SUPPORTED_LANGUAGES) {
|
||||
if (supportedLanguage.getLanguage().equals(languageTag)) {
|
||||
return supportedLanguage;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -79,6 +79,9 @@ public class AffiliationService {
|
||||
@ConfigProperty(name = "notif.affRequest.mail")
|
||||
List<String> mails;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
public Uni<List<AffiliationRequestModel>> getAllReq() {
|
||||
return repositoryRequest.listAll();
|
||||
}
|
||||
@ -92,7 +95,7 @@ public class AffiliationService {
|
||||
return Uni.createFrom().item(affModel)
|
||||
.invoke(Unchecked.consumer(model -> {
|
||||
if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) {
|
||||
throw new DBadRequestException("Saison non valid");
|
||||
throw new DBadRequestException(trad.t("saison.non.valid"));
|
||||
}
|
||||
}))
|
||||
.chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna(
|
||||
@ -110,7 +113,7 @@ public class AffiliationService {
|
||||
out, affModel.getSaison()))
|
||||
.onItem().invoke(Unchecked.consumer(count -> {
|
||||
if (count != 0 && unique) {
|
||||
throw new DBadRequestException("Demande d'affiliation déjà existante");
|
||||
throw new DBadRequestException(trad.t("demande.d.affiliation.deja.existante"));
|
||||
}
|
||||
}))
|
||||
)
|
||||
@ -118,28 +121,28 @@ public class AffiliationService {
|
||||
repository.count("club = ?1 and saison = ?2", club, affModel.getSaison())))
|
||||
.onItem().invoke(Unchecked.consumer(count -> {
|
||||
if (count != 0) {
|
||||
throw new DBadRequestException("Affiliation déjà existante");
|
||||
throw new DBadRequestException(trad.t("affiliation.deja.existante"));
|
||||
}
|
||||
}))
|
||||
.map(o -> affModel)
|
||||
.call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence",
|
||||
model.getM1_lincence()).count().invoke(Unchecked.consumer(count -> {
|
||||
if (count == 0) {
|
||||
throw new DBadRequestException("Licence membre n°1 inconnue");
|
||||
throw new DBadRequestException(trad.t("licence.membre.n.1.inconnue"));
|
||||
}
|
||||
})) : Uni.createFrom().nullItem())
|
||||
)
|
||||
.call(model -> ((model.getM2_lincence() != -1) ? combRepository.find("licence",
|
||||
model.getM2_lincence()).count().invoke(Unchecked.consumer(count -> {
|
||||
if (count == 0) {
|
||||
throw new DBadRequestException("Licence membre n°2 inconnue");
|
||||
throw new DBadRequestException(trad.t("licence.membre.n.2.inconnue"));
|
||||
}
|
||||
})) : Uni.createFrom().nullItem())
|
||||
)
|
||||
.call(model -> ((model.getM3_lincence() != -1) ? combRepository.find("licence",
|
||||
model.getM3_lincence()).count().invoke(Unchecked.consumer(count -> {
|
||||
if (count == 0) {
|
||||
throw new DBadRequestException("Licence membre n°3 inconnue");
|
||||
throw new DBadRequestException(trad.t("licence.membre.n.3.inconnue"));
|
||||
}
|
||||
})) : Uni.createFrom().nullItem())
|
||||
);
|
||||
@ -148,7 +151,7 @@ public class AffiliationService {
|
||||
public Uni<?> saveEdit(AffiliationRequestForm form) {
|
||||
return pre_save(form, false)
|
||||
.chain(model -> repositoryRequest.findById(form.getId())
|
||||
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
|
||||
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
|
||||
.chain(origine -> {
|
||||
origine.setName(model.getName());
|
||||
origine.setAddress(model.getAddress());
|
||||
@ -201,7 +204,7 @@ public class AffiliationService {
|
||||
LOGGER.debug(form.toString());
|
||||
|
||||
return repositoryRequest.findById(form.getId())
|
||||
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
|
||||
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
|
||||
.map(model -> {
|
||||
model.setName(form.getName());
|
||||
model.setState_id(form.getState_id());
|
||||
@ -306,7 +309,7 @@ public class AffiliationService {
|
||||
LOGGER.debug(form.toString());
|
||||
|
||||
return repositoryRequest.findById(form.getId())
|
||||
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
|
||||
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
|
||||
.chain(req ->
|
||||
clubRepository.find("StateId = ?1", form.getState_id()).firstResult()
|
||||
.chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model))
|
||||
@ -398,7 +401,7 @@ public class AffiliationService {
|
||||
|
||||
public Uni<SimpleReqAffiliation> getRequest(long id) {
|
||||
return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel)
|
||||
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
|
||||
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
|
||||
.call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> {
|
||||
if (c != null) {
|
||||
out.setClub(c.getId());
|
||||
@ -422,7 +425,7 @@ public class AffiliationService {
|
||||
|
||||
public Uni<List<SimpleAffiliation>> getAffiliation(long id) {
|
||||
return clubRepository.findById(id)
|
||||
.onItem().ifNull().failWith(new DNotFoundException("Club non trouvé"))
|
||||
.onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
|
||||
.call(model -> Mutiny.fetch(model.getAffiliations()))
|
||||
.chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId())
|
||||
.map(reqs -> reqs.stream().map(req ->
|
||||
@ -434,11 +437,11 @@ public class AffiliationService {
|
||||
|
||||
public Uni<SimpleAffiliation> setAffiliation(long id, int saison) {
|
||||
return clubRepository.findById(id)
|
||||
.onItem().ifNull().failWith(new DNotFoundException("Club non trouvé"))
|
||||
.onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
|
||||
.call(model -> Mutiny.fetch(model.getAffiliations()))
|
||||
.invoke(Unchecked.consumer(club -> {
|
||||
if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) {
|
||||
throw new DBadRequestException("Affiliation déjà existante");
|
||||
throw new DBadRequestException(trad.t("affiliation.deja.existante"));
|
||||
}
|
||||
}))
|
||||
.chain(club ->
|
||||
|
||||
@ -45,10 +45,13 @@ public class CategoryService {
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
public Uni<CategoryData> getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
|
||||
return repository.find("systemId = ?1 AND system = ?2", id, system)
|
||||
.firstResult()
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException("Category not found"))
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver")))
|
||||
.call(data -> permService.hasAdminViewPerm(securityCtx, data.getCompet()))
|
||||
.map(CategoryData::fromModel);
|
||||
}
|
||||
@ -64,7 +67,7 @@ public class CategoryService {
|
||||
.chain(o -> {
|
||||
if (o == null) {
|
||||
return competRepository.findById(data.getCompet())
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException("Competition not found"))
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found")))
|
||||
.call(o2 -> permService.hasEditPerm(securityCtx, o2))
|
||||
.chain(competitionModel -> {
|
||||
CategoryModel model = new CategoryModel();
|
||||
@ -139,7 +142,7 @@ public class CategoryService {
|
||||
.onItem().ifNotNull().call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet()))
|
||||
.onItem().ifNull().switchTo(
|
||||
() -> competRepository.findById(data.getCompet())
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException("Compet not found"))
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found")))
|
||||
.call(o -> permService.hasEditPerm(securityCtx, o))
|
||||
.map(o -> {
|
||||
CategoryModel model = new CategoryModel();
|
||||
@ -256,7 +259,7 @@ public class CategoryService {
|
||||
|
||||
public Uni<?> delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
|
||||
return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException("Category not found"))
|
||||
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver")))
|
||||
.call(o -> permService.hasEditPerm(securityCtx, o.getCompet()))
|
||||
.call(o -> Mutiny.fetch(o.getTree())
|
||||
.call(o2 -> o2.isEmpty() ? Uni.createFrom().nullItem() :
|
||||
|
||||
@ -63,6 +63,9 @@ public class ClubService {
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
|
||||
return VertxContextSupport.subscribeAndAwait(
|
||||
() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
|
||||
@ -130,7 +133,7 @@ public class ClubService {
|
||||
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null || m.getClub() == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
throw new DNotFoundException(trad.t("club.non.trouve"));
|
||||
}))
|
||||
.map(MembreModel::getClub)
|
||||
.call(club -> Mutiny.fetch(club.getContact()));
|
||||
@ -150,7 +153,7 @@ public class ClubService {
|
||||
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null || m.getClub() == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
throw new DNotFoundException(trad.t("club.non.trouve"));
|
||||
if (!securityCtx.isInClubGroup(m.getClub().getId()))
|
||||
throw new DForbiddenException();
|
||||
}))
|
||||
@ -166,7 +169,7 @@ public class ClubService {
|
||||
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null || m.getClub() == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
throw new DNotFoundException(trad.t("club.non.trouve"));
|
||||
if (!securityCtx.isInClubGroup(m.getClub().getId()))
|
||||
throw new DForbiddenException();
|
||||
}))
|
||||
@ -183,7 +186,7 @@ public class ClubService {
|
||||
ls.logUpdate("Contact(s)...", club);
|
||||
club.setContact(MAPPER.readValue(form.getContact(), typeRef));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new DBadRequestException("Erreur de format des contacts");
|
||||
throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
|
||||
}
|
||||
|
||||
ls.logChange("Lieux d'entrainements", club.getTraining_location(), form.getTraining_location(),
|
||||
@ -233,7 +236,7 @@ public class ClubService {
|
||||
ls.logUpdate("Contact(s)...", m);
|
||||
m.setContact(MAPPER.readValue(input.getContact(), typeRef));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new DBadRequestException("Erreur de format des contacts");
|
||||
throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
|
||||
}
|
||||
}
|
||||
return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
|
||||
|
||||
@ -97,6 +97,9 @@ public class CompetitionService {
|
||||
@CacheName("have-access")
|
||||
Cache cacheNoneAccess;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) {
|
||||
return permService.hasViewPerm(securityCtx, id).map(cm -> {
|
||||
CompetitionData out = CompetitionData.fromModelLight(cm);
|
||||
@ -192,7 +195,7 @@ public class CompetitionService {
|
||||
.invoke(Unchecked.consumer(combModel -> {
|
||||
if (!securityCtx.getRoles().contains("create_compet") && !securityCtx.getRoles()
|
||||
.contains("federation_admin"))
|
||||
throw new DForbiddenException("Vous ne pouvez pas créer de compétition");
|
||||
throw new DForbiddenException(trad.t("vous.ne.pouvez.pas.creer.de.competition"));
|
||||
}))
|
||||
.map(MembreModel::getClub)
|
||||
.chain(clubModel -> {
|
||||
@ -223,7 +226,8 @@ public class CompetitionService {
|
||||
keycloakService.getUser(data.getOwner()).map(UserRepresentation::getId).orElse(null))
|
||||
.invoke(Unchecked.consumer(newOwner -> {
|
||||
if (newOwner == null)
|
||||
throw new DBadRequestException("User " + data.getOwner() + " not found");
|
||||
throw new DBadRequestException(
|
||||
String.format(trad.t("user.not.found"), data.getOwner()));
|
||||
if (!newOwner.equals(model.getOwner())) {
|
||||
if (!securityCtx.roleHas("federation_admin")
|
||||
&& !securityCtx.roleHas("safca_super_admin")
|
||||
@ -342,7 +346,7 @@ public class CompetitionService {
|
||||
|| !securityCtx.isClubAdmin())
|
||||
throw new DForbiddenException();
|
||||
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
|
||||
throw new DBadRequestException("Inscription fermée");
|
||||
throw new DBadRequestException(trad.t("inscription.fermee"));
|
||||
}))
|
||||
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
|
||||
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
|
||||
@ -350,8 +354,7 @@ public class CompetitionService {
|
||||
if (!securityCtx.isInClubGroup(model.getClub().getId()))
|
||||
throw new DForbiddenException();
|
||||
if (c.getBanMembre().contains(model.getId()))
|
||||
throw new DForbiddenException(
|
||||
"Vous n'avez pas le droit d'inscrire ce membre (par décision de l'administrateur de la compétition)");
|
||||
throw new DForbiddenException(trad.t("insc.err1"));
|
||||
}))
|
||||
.chain(combModel -> updateRegister(data, c, combModel, false)))
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()));
|
||||
@ -361,13 +364,12 @@ public class CompetitionService {
|
||||
if (cm.getRegisterMode() != RegisterMode.FREE)
|
||||
throw new DForbiddenException();
|
||||
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
|
||||
throw new DBadRequestException("Inscription fermée");
|
||||
throw new DBadRequestException(trad.t("inscription.fermee"));
|
||||
}))
|
||||
.chain(c -> membreService.getByAccountId(securityCtx.getSubject())
|
||||
.invoke(Unchecked.consumer(model -> {
|
||||
if (c.getBanMembre().contains(model.getId()))
|
||||
throw new DForbiddenException(
|
||||
"Vous n'avez pas le droit de vous inscrire (par décision de l'administrateur de la compétition)");
|
||||
throw new DForbiddenException(trad.t("insc.err2"));
|
||||
}))
|
||||
.chain(combModel -> updateRegister(data, c, combModel, false)))
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, List.of()));
|
||||
@ -380,8 +382,7 @@ public class CompetitionService {
|
||||
.map(Unchecked.function(r -> {
|
||||
if (r != null) {
|
||||
if (!admin && r.isLockEdit())
|
||||
throw new DForbiddenException(
|
||||
"Modification bloquée par l'administrateur de la compétition");
|
||||
throw new DForbiddenException(trad.t("insc.err3"));
|
||||
r.setWeight(data.getWeight());
|
||||
r.setOverCategory(data.getOverCategory());
|
||||
r.setCategorie(
|
||||
@ -424,17 +425,17 @@ public class CompetitionService {
|
||||
return combRepository.find("licence = ?1", licence).firstResult()
|
||||
.invoke(Unchecked.consumer(combModel -> {
|
||||
if (combModel == null)
|
||||
throw new DForbiddenException("Licence " + licence + " non trouvé");
|
||||
throw new DForbiddenException(String.format(trad.t("licence.non.trouve"), licence));
|
||||
}));
|
||||
} else {
|
||||
if (fname == null || lname == null)
|
||||
return Uni.createFrom().failure(new DBadRequestException("Nom et prénom requis"));
|
||||
return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
|
||||
return combRepository.find("unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2)",
|
||||
lname,
|
||||
fname).firstResult()
|
||||
.invoke(Unchecked.consumer(combModel -> {
|
||||
if (combModel == null)
|
||||
throw new DForbiddenException("Combattant " + fname + " " + lname + " non trouvé");
|
||||
throw new DForbiddenException(String.format(trad.t("combattant.non.trouve"), fname, lname));
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -461,12 +462,12 @@ public class CompetitionService {
|
||||
|| !securityCtx.isClubAdmin())
|
||||
throw new DForbiddenException();
|
||||
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
|
||||
throw new DBadRequestException("Inscription fermée");
|
||||
throw new DBadRequestException(trad.t("inscription.fermee"));
|
||||
}))
|
||||
.call(cm -> membreService.getById(combId)
|
||||
.invoke(Unchecked.consumer(model -> {
|
||||
if (model == null)
|
||||
throw new DNotFoundException("Membre " + combId + " n'existe pas");
|
||||
throw new DNotFoundException(String.format(trad.t("le.membre.n.existe.pas"), combId));
|
||||
if (!securityCtx.isInClubGroup(model.getClub().getId()))
|
||||
throw new DForbiddenException();
|
||||
})))
|
||||
@ -478,7 +479,7 @@ public class CompetitionService {
|
||||
if (cm.getRegisterMode() != RegisterMode.FREE || !Objects.equals(model.getId(), combId))
|
||||
throw new DForbiddenException();
|
||||
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
|
||||
throw new DBadRequestException("Inscription fermée");
|
||||
throw new DBadRequestException(trad.t("inscription.fermee"));
|
||||
})))
|
||||
.chain(c -> deleteRegister(combId, c, false));
|
||||
}
|
||||
@ -486,7 +487,7 @@ public class CompetitionService {
|
||||
private Uni<Void> deleteRegister(Long combId, CompetitionModel c, boolean admin) {
|
||||
if (admin && combId < 0) {
|
||||
return competitionGuestRepository.find("competition = ?1 AND id = ?2", c, combId * -1).firstResult()
|
||||
.onFailure().transform(t -> new DBadRequestException("Combattant non inscrit"))
|
||||
.onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
|
||||
.call(Unchecked.function(
|
||||
model -> Panache.withTransaction(() -> competitionGuestRepository.delete(model))
|
||||
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
|
||||
@ -495,10 +496,10 @@ public class CompetitionService {
|
||||
.replaceWithVoid();
|
||||
}
|
||||
return registerRepository.find("competition = ?1 AND membre.id = ?2", c, combId).firstResult()
|
||||
.onFailure().transform(t -> new DBadRequestException("Combattant non inscrit"))
|
||||
.onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
|
||||
.call(Unchecked.function(registerModel -> {
|
||||
if (!admin && registerModel.isLockEdit())
|
||||
throw new DForbiddenException("Modification bloquée par l'administrateur de la compétition");
|
||||
throw new DForbiddenException(trad.t("insc.err3"));
|
||||
return Panache.withTransaction(() -> registerRepository.delete(registerModel))
|
||||
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom().voidItem());
|
||||
@ -533,7 +534,7 @@ public class CompetitionService {
|
||||
return permService.hasEditPerm(securityCtx, id)
|
||||
.invoke(Unchecked.consumer(cm -> {
|
||||
if (cm.getSystem() != CompetitionSystem.INTERNAL)
|
||||
throw new DBadRequestException("Competition is not INTERNAL");
|
||||
throw new DBadRequestException(trad.t("competition.is.not.internal"));
|
||||
}))
|
||||
.chain(competitionModel -> {
|
||||
SimpleCompetData data = SimpleCompetData.fromModel(competitionModel);
|
||||
@ -559,7 +560,7 @@ public class CompetitionService {
|
||||
return permService.hasEditPerm(securityCtx, data.getId())
|
||||
.invoke(Unchecked.consumer(cm -> {
|
||||
if (cm.getSystem() != CompetitionSystem.INTERNAL)
|
||||
throw new DBadRequestException("Competition is not INTERNAL");
|
||||
throw new DBadRequestException(trad.t("competition.is.not.internal"));
|
||||
}))
|
||||
.chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
ArrayList<String> admin = new ArrayList<>();
|
||||
@ -567,13 +568,13 @@ public class CompetitionService {
|
||||
for (String username : data.getAdmin()) {
|
||||
Optional<UserRepresentation> opt = keycloakService.getUser(username);
|
||||
if (opt.isEmpty())
|
||||
throw new DBadRequestException("User " + username + " not found");
|
||||
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
|
||||
admin.add(opt.get().getId());
|
||||
}
|
||||
for (String username : data.getTable()) {
|
||||
Optional<UserRepresentation> opt = keycloakService.getUser(username);
|
||||
if (opt.isEmpty())
|
||||
throw new DBadRequestException("User " + username + " not found");
|
||||
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
|
||||
table.add(opt.get().getId());
|
||||
}
|
||||
|
||||
@ -623,13 +624,13 @@ public class CompetitionService {
|
||||
for (String username : data.getAdmin()) {
|
||||
Optional<UserRepresentation> opt = keycloakService.getUser(username);
|
||||
if (opt.isEmpty())
|
||||
throw new DBadRequestException("User " + username + " not found");
|
||||
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
|
||||
admin.add(UUID.fromString(opt.get().getId()));
|
||||
}
|
||||
for (String username : data.getTable()) {
|
||||
Optional<UserRepresentation> opt = keycloakService.getUser(username);
|
||||
if (opt.isEmpty())
|
||||
throw new DBadRequestException("User " + username + " not found");
|
||||
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
|
||||
table.add(UUID.fromString(opt.get().getId()));
|
||||
}
|
||||
|
||||
|
||||
@ -47,6 +47,9 @@ public class LicenceService {
|
||||
@Inject
|
||||
CheckoutService checkoutService;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
|
||||
return combRepository.findById(id).invoke(checkPerm)
|
||||
.chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
||||
@ -157,7 +160,7 @@ public class LicenceService {
|
||||
return repository.list("membre.id IN ?1 AND saison = ?2 AND pay = FALSE", ids, Utils.getSaison())
|
||||
.invoke(Unchecked.consumer(models -> {
|
||||
if (models.size() != ids.size())
|
||||
throw new DBadRequestException("Erreur lors de la sélection des membres");
|
||||
throw new DBadRequestException(trad.t("erreur.lors.de.la.selection.des.membres"));
|
||||
}))
|
||||
.call(models -> {
|
||||
Uni<?> uni = Uni.createFrom().nullItem();
|
||||
@ -174,7 +177,7 @@ public class LicenceService {
|
||||
.call(__ -> checkoutService.canDeleteLicence(id)
|
||||
.invoke(Unchecked.consumer(b -> {
|
||||
if (!b) throw new DBadRequestException(
|
||||
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
|
||||
trad.t("licence.rm.err1"));
|
||||
})))
|
||||
.call(model -> ls.logADelete(model))
|
||||
.chain(model -> Panache.withTransaction(() -> repository.delete(model)));
|
||||
@ -186,7 +189,7 @@ public class LicenceService {
|
||||
return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count()
|
||||
.invoke(Unchecked.consumer(count -> {
|
||||
if (count > 0)
|
||||
throw new DBadRequestException("Licence déjà demandée");
|
||||
throw new DBadRequestException(trad.t("licence.deja.demandee"));
|
||||
})).chain(__ -> combRepository.findById(id).chain(membreModel2 -> {
|
||||
LicenceModel model = new LicenceModel();
|
||||
model.setClub_id((membreModel2.getClub() == null) ? null : membreModel2.getClub().getId());
|
||||
@ -215,13 +218,13 @@ public class LicenceService {
|
||||
.call(__ -> checkoutService.canDeleteLicence(id)
|
||||
.invoke(Unchecked.consumer(b -> {
|
||||
if (!b) throw new DBadRequestException(
|
||||
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
|
||||
trad.t("licence.rm.err1"));
|
||||
})))
|
||||
.invoke(Unchecked.consumer(licenceModel -> {
|
||||
if (licenceModel.isValidate())
|
||||
throw new DBadRequestException("Impossible de supprimer une licence déjà validée");
|
||||
throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.validee"));
|
||||
if (licenceModel.isPay())
|
||||
throw new DBadRequestException("Impossible de supprimer une licence déjà payée");
|
||||
throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.payee"));
|
||||
}))
|
||||
.call(model -> ls.logADelete(model))
|
||||
.chain(__ -> Panache.withTransaction(() -> repository.deleteById(id)));
|
||||
|
||||
@ -62,6 +62,9 @@ public class MembreService {
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
public SimpleCombModel find(int licence, String np) throws Throwable {
|
||||
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
|
||||
repository.find(
|
||||
@ -177,7 +180,7 @@ public class MembreService {
|
||||
|
||||
Sort sort = getSort(order);
|
||||
if (sort == null)
|
||||
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
|
||||
return Uni.createFrom().failure(new DInternalError(trad.t("erreur.lors.calcul.du.trie")));
|
||||
|
||||
String finalSearch = search;
|
||||
return getLicenceListe(licenceRequest, payState)
|
||||
@ -196,7 +199,7 @@ public class MembreService {
|
||||
.call(result -> query.count().invoke(result::setResult_count))
|
||||
.call(result -> query.pageCount()
|
||||
.invoke(Unchecked.consumer(pages -> {
|
||||
if (page > pages) throw new DBadRequestException("Page out of range");
|
||||
if (page > pages) throw new DBadRequestException(trad.t("page.out.of.range"));
|
||||
}))
|
||||
.invoke(result::setPage_count))
|
||||
.call(result -> query.page(Page.of(page, limit)).list()
|
||||
@ -241,8 +244,8 @@ public class MembreService {
|
||||
for (MembreModel membreModel : membres) {
|
||||
if (!Objects.equals(membreModel.getClub(), clubModel.get())) {
|
||||
LOGGER.info("Similar membres found: " + membreModel);
|
||||
throw new DForbiddenException(
|
||||
"Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club");
|
||||
throw new DForbiddenException(String.format(trad.t("le.membre.appartient.pas.a.votre.club"),
|
||||
membreModel.getLicence()));
|
||||
}
|
||||
}
|
||||
Uni<Void> uniResult = Uni.createFrom().voidItem();
|
||||
@ -269,7 +272,8 @@ public class MembreService {
|
||||
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
|
||||
LOGGER.info("Similar membres found: " + model);
|
||||
throw new DBadRequestException(
|
||||
"Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname());
|
||||
String.format(trad.t("email.deja.utilise.par"), model.getEmail(),
|
||||
model.getFname(), model.getLname()));
|
||||
}
|
||||
|
||||
if (StringSimilarity.similarity(model.getLname().toUpperCase(),
|
||||
@ -277,7 +281,8 @@ public class MembreService {
|
||||
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
|
||||
LOGGER.info("Similar membres found: " + model);
|
||||
throw new DBadRequestException(
|
||||
"Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname());
|
||||
String.format(trad.t("email.deja.utilise.par"), model.getEmail(),
|
||||
model.getFname(), model.getLname()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,8 +293,8 @@ public class MembreService {
|
||||
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) {
|
||||
LOGGER.info("Similar membres found: " + model);
|
||||
throw new DBadRequestException(
|
||||
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (tentative de changement non-autotiser de nom sur la licence "
|
||||
+ model.getLicence() + " pour " + model.getFname() + " " + model.getFname() + ")");
|
||||
String.format(trad.t("try.edit.licence"), model.getLicence(), model.getFname(),
|
||||
model.getFname()));
|
||||
}
|
||||
|
||||
ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model);
|
||||
@ -363,7 +368,7 @@ public class MembreService {
|
||||
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0 && !membre.getEmail().isBlank())
|
||||
throw new DBadRequestException("Email déjà utilisé");
|
||||
throw new DBadRequestException(trad.t("email.deja.utilise"));
|
||||
})))
|
||||
.chain(membreModel -> clubRepository.findById(membre.getClub())
|
||||
.map(club -> new Pair<>(membreModel, club)))
|
||||
@ -385,7 +390,7 @@ public class MembreService {
|
||||
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0 && !membre.getEmail().isBlank())
|
||||
throw new DBadRequestException("Email déjà utilisé");
|
||||
throw new DBadRequestException(trad.t("email.deja.utilise"));
|
||||
})))
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
|
||||
@ -394,7 +399,7 @@ public class MembreService {
|
||||
membre.getLname().toUpperCase()) > 3 || StringSimilarity.similarity(
|
||||
membreModel.getFname().toUpperCase(), membre.getFname().toUpperCase()) > 3) {
|
||||
throw new DBadRequestException(
|
||||
"Pour enregistrer un nouveau membre, veuillez utilisez le bouton prévue a cette effet.");
|
||||
trad.t("regiter.new.membre"));
|
||||
}
|
||||
}))
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
@ -403,7 +408,7 @@ public class MembreService {
|
||||
else if (securityCtx.roleHas("club_secretaire")) source = RoleAsso.SECRETAIRE;
|
||||
else if (securityCtx.roleHas("club_respo_intra")) source = RoleAsso.MEMBREBUREAU;
|
||||
if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level >= source.level)
|
||||
throw new DForbiddenException("Permission insuffisante");
|
||||
throw new DForbiddenException(trad.t("permission.insuffisante"));
|
||||
}))
|
||||
.onItem().transform(target -> {
|
||||
if (!securityCtx.getSubject().equals(target.getUserId())) {
|
||||
@ -477,7 +482,7 @@ public class MembreService {
|
||||
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0 && input.getEmail() != null && !input.getEmail().isBlank())
|
||||
throw new DBadRequestException("Email déjà utilisé");
|
||||
throw new DBadRequestException(trad.t("email.deja.utilise"));
|
||||
})))
|
||||
.chain(clubModel -> {
|
||||
MembreModel model = getMembreModel(input, clubModel);
|
||||
@ -493,7 +498,7 @@ public class MembreService {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0) throw new DBadRequestException("Email déjà utilisé");
|
||||
if (c > 0) throw new DBadRequestException(trad.t("email.deja.utilise"));
|
||||
})))
|
||||
.call(membreModel ->
|
||||
repository.count(
|
||||
@ -501,7 +506,7 @@ public class MembreService {
|
||||
input.getLname(), input.getFname(), membreModel.getClub())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0)
|
||||
throw new DBadRequestException("Membre déjà existent");
|
||||
throw new DBadRequestException(trad.t("membre.deja.existent"));
|
||||
})))
|
||||
.chain(membreModel -> {
|
||||
MembreModel model = getMembreModel(input, membreModel.getClub());
|
||||
@ -534,13 +539,13 @@ public class MembreService {
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (membreModel.getLicence() != null) {
|
||||
throw new DBadRequestException(
|
||||
"Impossible de supprimer un membre qui a déjà un numéro de licence");
|
||||
trad.t("membre.rm.err1"));
|
||||
}
|
||||
}))
|
||||
.call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count()
|
||||
.invoke(Unchecked.consumer(l -> {
|
||||
if (l > 0)
|
||||
throw new DBadRequestException("Impossible de supprimer un membre avec des licences");
|
||||
throw new DBadRequestException(trad.t("membre.rm.err2"));
|
||||
})))
|
||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
||||
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
|
||||
@ -586,7 +591,7 @@ public class MembreService {
|
||||
public Uni<MeData> getMembre(String subject) {
|
||||
MeData meData = new MeData();
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(meData::setMembre)
|
||||
.invoke(m -> meData.setMembre(m, trad))
|
||||
.call(membreModel -> Mutiny.fetch(membreModel.getLicences())
|
||||
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
|
||||
.invoke(meData::setLicences))
|
||||
|
||||
@ -46,6 +46,9 @@ public class PDFService {
|
||||
@ConfigProperty(name = "pdf-maker.sign-file")
|
||||
String sign_file;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
|
||||
public Uni<Response> getLicencePdf(String subject) {
|
||||
return getLicencePdf(combRepository.find("userId = ?1", subject).firstResult()
|
||||
@ -58,7 +61,7 @@ public class PDFService {
|
||||
LicenceModel licence = m.getLicences().stream()
|
||||
.filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DNotFoundException("Pas de licence pour la saison en cours"));
|
||||
.orElseThrow(() -> new DNotFoundException(trad.t("pas.de.licence.pour.la.saison.en.cours")));
|
||||
|
||||
try {
|
||||
byte[] buff = make_pdf(m, licence);
|
||||
@ -119,7 +122,7 @@ public class PDFService {
|
||||
combRepository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null || m.getClub() == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
throw new DNotFoundException(trad.t("club.non.trouve"));
|
||||
}))
|
||||
.map(MembreModel::getClub)
|
||||
.call(m -> Mutiny.fetch(m.getAffiliations())));
|
||||
@ -130,7 +133,7 @@ public class PDFService {
|
||||
clubRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(m -> {
|
||||
if (m == null)
|
||||
throw new DNotFoundException("Club non trouvé");
|
||||
throw new DNotFoundException(trad.t("club.non.trouve"));
|
||||
}))
|
||||
.call(m -> Mutiny.fetch(m.getAffiliations())));
|
||||
}
|
||||
@ -141,7 +144,7 @@ public class PDFService {
|
||||
.map(Unchecked.function(m -> {
|
||||
if (m.getAffiliations().stream()
|
||||
.noneMatch(licenceModel -> licenceModel.getSaison() == Utils.getSaison()))
|
||||
throw new DNotFoundException("Pas d'affiliation pour la saison en cours");
|
||||
throw new DNotFoundException(trad.t("pas.d.affiliation.pour.la.saison.en.cours"));
|
||||
|
||||
try {
|
||||
byte[] buff = make_pdf(m);
|
||||
|
||||
@ -45,7 +45,8 @@ public class ResultService {
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String");
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private static final HashMap<Long, String> combTempIds = new HashMap<>();
|
||||
|
||||
@ -289,17 +290,17 @@ public class ResultService {
|
||||
if (register.isPresent()) {
|
||||
categorie = register.get().getCategorie2();
|
||||
ClubModel club = register.get().getClub2();
|
||||
clubName = (club == null) ? BUNDLE.getString("no.licence") : club.getName();
|
||||
clubName = (club == null) ? trad.t("no.licence") : club.getName();
|
||||
} else if (comb instanceof CompetitionGuestModel guestModel) {
|
||||
categorie = guestModel.getCategorie();
|
||||
clubName = guestModel.getClub();
|
||||
} else if (comb instanceof MembreModel model) {
|
||||
categorie = model.getCategorie();
|
||||
clubName = (model.getClub() == null) ? BUNDLE.getString(
|
||||
"no.licence") : model.getClub().getName();
|
||||
clubName = (model.getClub() == null) ? trad.t("no.licence")
|
||||
: model.getClub().getName();
|
||||
}
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
|
||||
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
@ -364,7 +365,7 @@ public class ResultService {
|
||||
|
||||
Long id = getCombTempId(combTempId);
|
||||
if (id == null) {
|
||||
return Uni.createFrom().failure(new DForbiddenException("Comb not found"));
|
||||
return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found")));
|
||||
}
|
||||
|
||||
Uni<List<MatchModel>> uni;
|
||||
@ -373,13 +374,13 @@ public class ResultService {
|
||||
uuid, privacy).firstResult()
|
||||
.chain(Unchecked.function(registerModel -> {
|
||||
if (registerModel == null)
|
||||
throw new DBadRequestException("Combattant non inscrit");
|
||||
throw new DBadRequestException(trad.t("combattant.non.inscrit"));
|
||||
|
||||
builder.name(Utils.getFullName(registerModel.getMembre()));
|
||||
builder.club((registerModel.getClub2() == null) ? BUNDLE.getString(
|
||||
"no.licence") : registerModel.getClub2().getName());
|
||||
builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2()
|
||||
.getName(BUNDLE));
|
||||
builder.club((registerModel.getClub2() == null) ? trad.t("no.licence") :
|
||||
registerModel.getClub2().getName());
|
||||
builder.cat((registerModel.getCategorie2() == null) ? "---" :
|
||||
registerModel.getCategorie2().getName(trad));
|
||||
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid,
|
||||
registerModel.getMembre());
|
||||
@ -389,8 +390,8 @@ public class ResultService {
|
||||
.chain(guestModel -> {
|
||||
builder.name(Utils.getFullName(guestModel));
|
||||
builder.club(guestModel.getClub());
|
||||
builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie()
|
||||
.getName(BUNDLE));
|
||||
builder.cat(
|
||||
(guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad));
|
||||
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)",
|
||||
uuid, guestModel);
|
||||
@ -529,13 +530,13 @@ public class ResultService {
|
||||
if (id < 0) {
|
||||
String clubName = getClubTempId(id);
|
||||
if (clubName == null) {
|
||||
return Uni.createFrom().failure(new DForbiddenException("Club not found"));
|
||||
return Uni.createFrom().failure(new DForbiddenException(trad.t("club.not.found")));
|
||||
}
|
||||
|
||||
return competitionGuestRepository.list("competition.uuid = ?1 AND club = ?2", uuid, clubName)
|
||||
.call(list -> {
|
||||
if (list.isEmpty())
|
||||
return Uni.createFrom().failure(new DBadRequestException("Club not found"));
|
||||
return Uni.createFrom().failure(new DBadRequestException(trad.t("club.not.found")));
|
||||
return Uni.createFrom().voidItem();
|
||||
})
|
||||
.chain(guests -> matchRepository.list(
|
||||
@ -584,7 +585,7 @@ public class ResultService {
|
||||
categorie = model.getCategorie();
|
||||
}
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
|
||||
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
@ -668,7 +669,8 @@ public class ResultService {
|
||||
uuid, securityCtx.getSubject())
|
||||
.chain(c2 -> {
|
||||
if (c2 > 0) return Uni.createFrom().item(m);
|
||||
return Uni.createFrom().failure(new DForbiddenException("Access denied"));
|
||||
return Uni.createFrom().failure(new DForbiddenException(
|
||||
trad.t("access.denied")));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@ -676,7 +678,7 @@ public class ResultService {
|
||||
securityCtx.getSubject())
|
||||
.chain(c2 -> {
|
||||
if (c2 > 0) return Uni.createFrom().item(m);
|
||||
return Uni.createFrom().failure(new DForbiddenException("Access denied"));
|
||||
return Uni.createFrom().failure(new DForbiddenException(trad.t("access.denied")));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@RequestScoped
|
||||
public class TradService {
|
||||
|
||||
@Inject
|
||||
Instance<ContainerRequestContext> requestContextInstance;
|
||||
|
||||
public static final Locale fallbackLocale = Locale.FRANCE;
|
||||
|
||||
public String t(String key) {
|
||||
return translate(key);
|
||||
}
|
||||
|
||||
public String translate(String key) {
|
||||
ContainerRequestContext requestContext = requestContextInstance.get();
|
||||
Locale userLocale = (Locale) requestContext.getProperty("userLocale");
|
||||
|
||||
|
||||
try {
|
||||
ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale);
|
||||
return messages.getString(key);
|
||||
} catch (MissingResourceException e) {
|
||||
try {
|
||||
ResourceBundle fallbackMessages = ResourceBundle.getBundle("lang.messages", fallbackLocale);
|
||||
return fallbackMessages.getString(key);
|
||||
} catch (MissingResourceException ex) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.client.SirenService;
|
||||
import fr.titionfire.ffsaf.rest.client.StateIdService;
|
||||
import fr.titionfire.ffsaf.rest.data.AssoData;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
@ -19,6 +21,9 @@ public class AssoEndpoints {
|
||||
@RestClient
|
||||
SirenService sirenService;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
@GET
|
||||
@Path("state_id/{stateId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ -28,9 +33,9 @@ public class AssoEndpoints {
|
||||
stateId).chain(stateIdService::getAssoDataFromUnit)).onFailure().transform(throwable -> {
|
||||
if (throwable instanceof WebApplicationException exception) {
|
||||
if (exception.getResponse().getStatus() == 404)
|
||||
return new DNotFoundException("Service momentanément indisponible");
|
||||
return new DNotFoundException(trad.t("service.momentanement.indisponible"));
|
||||
if (exception.getResponse().getStatus() == 400)
|
||||
return new DNotFoundException("Asso introuvable");
|
||||
return new DNotFoundException(trad.t("asso.introuvable"));
|
||||
}
|
||||
return throwable;
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.rest;
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.domain.service.ClubService;
|
||||
import fr.titionfire.ffsaf.domain.service.PDFService;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
|
||||
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
||||
import fr.titionfire.ffsaf.rest.data.*;
|
||||
@ -53,6 +54,9 @@ public class ClubEndpoints {
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
Consumer<ClubModel> checkPerm = Unchecked.consumer(clubModel -> {
|
||||
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(clubModel.getId()))
|
||||
throw new DForbiddenException();
|
||||
@ -84,7 +88,7 @@ public class ClubEndpoints {
|
||||
@Operation(summary = "Renvoie les types de contacts pour les clubs", description = "Renvoie la liste des types de " +
|
||||
"contacts possibles pour les clubs")
|
||||
public Uni<HashMap<String, String>> getConcatType() {
|
||||
return Uni.createFrom().item(Contact.toSite());
|
||||
return Uni.createFrom().item(Contact.toSite(trad));
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -125,7 +129,7 @@ public class ClubEndpoints {
|
||||
public Uni<SimpleClub> getById(
|
||||
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
|
||||
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel)
|
||||
.invoke(m -> m.setContactMap(Contact.toSite()));
|
||||
.invoke(m -> m.setContactMap(Contact.toSite(trad)));
|
||||
}
|
||||
|
||||
@PUT
|
||||
@ -218,7 +222,7 @@ public class ClubEndpoints {
|
||||
})
|
||||
public Uni<SimpleClub> getOfUser() {
|
||||
return clubService.getOfUser(securityCtx).map(SimpleClub::fromModel)
|
||||
.invoke(m -> m.setContactMap(Contact.toSite()));
|
||||
.invoke(m -> m.setContactMap(Contact.toSite(trad)));
|
||||
}
|
||||
|
||||
@PUT
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.utils.ResultPrivacy;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
@ -47,19 +48,19 @@ public class MeData {
|
||||
@Schema(description = "La liste des séléctions du membre.")
|
||||
private List<SimpleSelection> selections;
|
||||
|
||||
public void setMembre(MembreModel membreModel) {
|
||||
public void setMembre(MembreModel membreModel, TradService trad) {
|
||||
this.id = membreModel.getId();
|
||||
this.lname = membreModel.getLname();
|
||||
this.fname = membreModel.getFname();
|
||||
this.categorie = membreModel.getCategorie() == null ? "catégorie inconnue" : membreModel.getCategorie().getName();
|
||||
this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName();
|
||||
this.genre = membreModel.getGenre().str;
|
||||
this.categorie = membreModel.getCategorie() == null ? trad.t("categorie.inconnue") : membreModel.getCategorie().getName(trad);
|
||||
this.club = membreModel.getClub() == null ? trad.t("sans.club") : membreModel.getClub().getName();
|
||||
this.genre = membreModel.getGenre().getString(trad);
|
||||
this.licence = membreModel.getLicence();
|
||||
this.country = membreModel.getCountry();
|
||||
this.birth_date = membreModel.getBirth_date();
|
||||
this.email = membreModel.getEmail();
|
||||
this.role = membreModel.getRole().str;
|
||||
this.grade_arbitrage = membreModel.getGrade_arbitrage().str;
|
||||
this.role = membreModel.getRole().getString(trad);
|
||||
this.grade_arbitrage = membreModel.getGrade_arbitrage().getString(trad);
|
||||
this.resultPrivacy = membreModel.getResultPrivacy();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
@ -34,6 +35,22 @@ public enum Categorie {
|
||||
};
|
||||
}
|
||||
|
||||
public String getName(TradService tradService) {
|
||||
return switch (this) {
|
||||
case SUPER_MINI -> tradService.translate("Cat.SUPER_MINI");
|
||||
case MINI_POUSSIN -> tradService.translate("Cat.MINI_POUSSIN");
|
||||
case POUSSIN -> tradService.translate("Cat.POUSSIN");
|
||||
case BENJAMIN -> tradService.translate("Cat.BENJAMIN");
|
||||
case MINIME -> tradService.translate("Cat.MINIME");
|
||||
case CADET -> tradService.translate("Cat.CADET");
|
||||
case JUNIOR -> tradService.translate("Cat.JUNIOR");
|
||||
case SENIOR1 -> tradService.translate("Cat.SENIOR1");
|
||||
case SENIOR2 -> tradService.translate("Cat.SENIOR2");
|
||||
case VETERAN1 -> tradService.translate("Cat.VETERAN1");
|
||||
case VETERAN2 -> tradService.translate("Cat.VETERAN2");
|
||||
};
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return switch (this) {
|
||||
case SUPER_MINI -> "Super Mini";
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
@RegisterForReflection
|
||||
public enum Contact {
|
||||
COURRIEL("Courriel"),
|
||||
TELEPHONE("Téléphone"),
|
||||
SITE("Site web"),
|
||||
FACEBOOK("Facebook"),
|
||||
INSTAGRAM("Instagram"),
|
||||
AUTRE("Autre");
|
||||
COURRIEL,
|
||||
TELEPHONE,
|
||||
SITE,
|
||||
FACEBOOK,
|
||||
INSTAGRAM,
|
||||
AUTRE;
|
||||
|
||||
public String name;
|
||||
|
||||
Contact(String name) {
|
||||
this.name = name;
|
||||
public String getName(TradService trad) {
|
||||
return switch (this) {
|
||||
case COURRIEL -> trad.translate("Contact.COURRIEL");
|
||||
case TELEPHONE -> trad.translate("Contact.TELEPHONE");
|
||||
case SITE -> trad.translate("Contact.SITE");
|
||||
case FACEBOOK -> trad.translate("Contact.FACEBOOK");
|
||||
case INSTAGRAM -> trad.translate("Contact.INSTAGRAM");
|
||||
case AUTRE -> trad.translate("Contact.AUTRE");
|
||||
};
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static HashMap<String, String> toSite() {
|
||||
public static HashMap<String, String> toSite(TradService trad) {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
for (Contact contact : Contact.values()) {
|
||||
map.put(contact.toString(), contact.name);
|
||||
map.put(contact.toString(), contact.getName(trad));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
@ -27,8 +28,17 @@ public enum Genre {
|
||||
}
|
||||
}
|
||||
|
||||
public String getString(TradService trad) {
|
||||
return switch (this) {
|
||||
case H -> trad.translate("Genre.Homme");
|
||||
case F -> trad.translate("Genre.Femme");
|
||||
case NA -> trad.translate("Genre.NA");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return str;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
@ -14,6 +15,14 @@ public enum GradeArbitrage {
|
||||
this.str = name;
|
||||
}
|
||||
|
||||
public String getString(TradService trad) {
|
||||
return switch (this) {
|
||||
case NA -> trad.translate("GradeArbitrage.NA");
|
||||
case ASSESSEUR -> trad.translate("GradeArbitrage.ASSESSEUR");
|
||||
case ARBITRE -> trad.translate("GradeArbitrage.ARBITRE");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return str;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
@ -21,6 +22,19 @@ public enum RoleAsso {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getString(TradService trad) {
|
||||
return switch (this){
|
||||
case MEMBRE -> trad.translate("RoleAsso.MEMBRE");
|
||||
case PRESIDENT -> trad.translate("RoleAsso.PRESIDENT");
|
||||
case VPRESIDENT -> trad.translate("RoleAsso.VPRESIDENT");
|
||||
case SECRETAIRE -> trad.translate("RoleAsso.SECRETAIRE");
|
||||
case VSECRETAIRE -> trad.translate("RoleAsso.VSECRETAIRE");
|
||||
case TRESORIER -> trad.translate("RoleAsso.TRESORIER");
|
||||
case VTRESORIER -> trad.translate("RoleAsso.VTRESORIER");
|
||||
case MEMBREBUREAU -> trad.translate("RoleAsso.MEMBREBUREAU");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return str;
|
||||
|
||||
@ -5,6 +5,7 @@ import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CardboardRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
@ -32,13 +33,16 @@ public class RCardboard {
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Matche non trouver");
|
||||
throw new DNotFoundException(trad.t("matche.non.trouver"));
|
||||
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.data.model.TreeModel;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.utils.TreeNode;
|
||||
@ -46,13 +47,16 @@ public class RCategorie {
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
|
||||
return categoryRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver"));
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import fr.titionfire.ffsaf.data.model.*;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
|
||||
@ -47,13 +48,16 @@ public class RMatch {
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Matche non trouver");
|
||||
throw new DNotFoundException(trad.t("matche.non.trouver"));
|
||||
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
}));
|
||||
}
|
||||
|
||||
@ -81,9 +85,9 @@ public class RMatch {
|
||||
return categoryRepository.findById(m.categorie)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver"));
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
}))
|
||||
.chain(categoryModel -> creatMatch(categoryModel, m))
|
||||
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)))
|
||||
@ -294,9 +298,9 @@ public class RMatch {
|
||||
return categoryRepository.findById(data.categorie)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver"));
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
}))
|
||||
.call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
(Panache.withTransaction(
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
filtre.all=--tout--
|
||||
no.licence=Non licenci\u00E9
|
||||
|
||||
|
||||
# Categories
|
||||
Cat.SUPER_MINI=Super Mini
|
||||
Cat.MINI_POUSSIN=Mini Poussin
|
||||
Cat.POUSSIN= Poussin
|
||||
Cat.BENJAMIN=Benjamin
|
||||
Cat.MINIME=Minime
|
||||
Cat.CADET=Cadet
|
||||
Cat.JUNIOR=Junior
|
||||
Cat.SENIOR1=Senior 1
|
||||
Cat.SENIOR2=Senior 2
|
||||
Cat.VETERAN1=V\u00E9t\u00E9ran 1
|
||||
Cat.VETERAN2=V\u00E9t\u00E9ran 2
|
||||
87
src/main/resources/lang/messages_en.properties
Normal file
87
src/main/resources/lang/messages_en.properties
Normal file
@ -0,0 +1,87 @@
|
||||
filtre.all=--all--
|
||||
no.licence=Not licensed
|
||||
Cat.SUPER_MINI=Super Mini
|
||||
Cat.MINI_POUSSIN=Mini Chick
|
||||
Cat.POUSSIN=Chick
|
||||
Cat.BENJAMIN=Benjamin
|
||||
Cat.MINIME=Minime
|
||||
Cat.CADET=Cadet
|
||||
Cat.JUNIOR=Junior
|
||||
Cat.SENIOR1=Senior 1
|
||||
Cat.SENIOR2=Senior 2
|
||||
Cat.VETERAN1=Veteran 1
|
||||
Cat.VETERAN2=Veteran 2
|
||||
access.denied=Access denied
|
||||
club.not.found=Club not found
|
||||
combattant.non.inscrit=Fighter not registered
|
||||
comb.not.found=Fighter not found
|
||||
matche.non.trouver=Match not found
|
||||
permission.denied=Permission denied
|
||||
categorie.non.trouver=Category not found
|
||||
|
||||
RoleAsso.MEMBRE=Member
|
||||
RoleAsso.PRESIDENT=President
|
||||
RoleAsso.VPRESIDENT=Vice-President
|
||||
RoleAsso.SECRETAIRE=Secretary
|
||||
RoleAsso.VSECRETAIRE=Vice-Secretary
|
||||
RoleAsso.TRESORIER=Treasurer
|
||||
RoleAsso.VTRESORIER=Vice-Treasurer
|
||||
RoleAsso.MEMBREBUREAU=Board Member
|
||||
|
||||
sans.club=No club
|
||||
categorie.inconnue=Unknown category
|
||||
GradeArbitrage.NA=N/A
|
||||
GradeArbitrage.ASSESSEUR=Assessor
|
||||
GradeArbitrage.ARBITRE=Referee
|
||||
Genre.Homme=Male
|
||||
Genre.Femme=Female
|
||||
Genre.NA=Not defined
|
||||
|
||||
Contact.COURRIEL=Email
|
||||
Contact.TELEPHONE=Phone
|
||||
Contact.SITE=Website
|
||||
Contact.FACEBOOK=Facebook
|
||||
Contact.INSTAGRAM=Instagram
|
||||
Contact.AUTRE=Other
|
||||
|
||||
service.momentanement.indisponible=Service temporarily unavailable
|
||||
asso.introuvable=Association not found
|
||||
erreur.lors.calcul.du.trie=Error during sorting calculation
|
||||
page.out.of.range=Page out of range
|
||||
le.membre.appartient.pas.a.votre.club=Member no. %d does not belong to your club
|
||||
email.deja.utilise.par=Email '%s' already used by %s %s
|
||||
try.edit.licence=To register a new member, please leave the license field blank. (Unauthorized attempt to change the name on license %d for %s %s)
|
||||
email.deja.utilise=Email already in use
|
||||
regiter.new.membre=To register a new member, please use the dedicated button.
|
||||
permission.insuffisante=Insufficient permission
|
||||
membre.deja.existent=Member already exists
|
||||
membre.rm.err1=Cannot delete a member who already has a license number
|
||||
membre.rm.err2=Cannot delete a member with licenses
|
||||
pas.de.licence.pour.la.saison.en.cours=No license for the current season
|
||||
club.non.trouve=Club not found
|
||||
pas.d.affiliation.pour.la.saison.en.cours=No affiliation for the current season
|
||||
erreur.lors.de.la.selection.des.membres=Error during member selection
|
||||
licence.rm.err1=Cannot delete a license for which payment is in progress
|
||||
licence.deja.demandee=License already requested
|
||||
impossible.de.supprimer.une.licence.deja.validee=Cannot delete an already validated license
|
||||
impossible.de.supprimer.une.licence.deja.payee=Cannot delete an already paid license
|
||||
vous.ne.pouvez.pas.creer.de.competition=You cannot create a competition
|
||||
user.not.found=User %s not found
|
||||
inscription.fermee=Registration closed
|
||||
insc.err1=You do not have the right to register this member (by decision of the competition administrator)
|
||||
insc.err2=You do not have the right to register (by decision of the competition administrator)
|
||||
insc.err3=Modification blocked by the competition administrator
|
||||
licence.non.trouve=License %s not found
|
||||
nom.et.prenom.requis=Last name and first name required
|
||||
combattant.non.trouve=Fighter %s %s not found
|
||||
le.membre.n.existe.pas=Member %d does not exist
|
||||
competition.is.not.internal=Competition is not INTERNAL
|
||||
erreur.de.format.des.contacts=Contact format error
|
||||
competition.not.found=Competition not found
|
||||
saison.non.valid=Invalid season
|
||||
demande.d.affiliation.deja.existante=Affiliation request already exists
|
||||
affiliation.deja.existante=Affiliation already exists
|
||||
licence.membre.n.1.inconnue=License member no. 1 unknown
|
||||
licence.membre.n.2.inconnue=License member no. 2 unknown
|
||||
licence.membre.n.3.inconnue=License member no. 3 unknown
|
||||
demande.d.affiliation.non.trouve=Affiliation request not found
|
||||
83
src/main/resources/lang/messages_fr.properties
Normal file
83
src/main/resources/lang/messages_fr.properties
Normal file
@ -0,0 +1,83 @@
|
||||
filtre.all=--Tous--
|
||||
no.licence=Non licencié
|
||||
Cat.SUPER_MINI=Super Mini
|
||||
Cat.MINI_POUSSIN=Mini-Poussin
|
||||
Cat.POUSSIN=Poussin
|
||||
Cat.BENJAMIN=Benjamin
|
||||
Cat.MINIME=Minime
|
||||
Cat.CADET=Cadet
|
||||
Cat.JUNIOR=Junior
|
||||
Cat.SENIOR1=Senior 1
|
||||
Cat.SENIOR2=Senior 2
|
||||
Cat.VETERAN1=Vétéran 1
|
||||
Cat.VETERAN2=Vétéran 2
|
||||
access.denied=Accès refusé
|
||||
club.not.found=Club introuvable
|
||||
combattant.non.inscrit=Combattant non inscrit
|
||||
comb.not.found=Combat introuvable
|
||||
matche.non.trouver=Match introuvable
|
||||
permission.denied=Permission refusée
|
||||
categorie.non.trouver=Catégorie introuvable
|
||||
RoleAsso.MEMBRE=Membre
|
||||
RoleAsso.PRESIDENT=Président
|
||||
RoleAsso.VPRESIDENT=Vice-Président
|
||||
RoleAsso.SECRETAIRE=Secrétaire
|
||||
RoleAsso.VSECRETAIRE=Vice-Secrétaire
|
||||
RoleAsso.TRESORIER=Trésorier
|
||||
RoleAsso.VTRESORIER=Vice-Trésorier
|
||||
RoleAsso.MEMBREBUREAU=Membre bureau
|
||||
sans.club=Sans club
|
||||
categorie.inconnue=catégorie inconnue
|
||||
GradeArbitrage.NA=N/A
|
||||
GradeArbitrage.ASSESSEUR=Assesseur
|
||||
GradeArbitrage.ARBITRE=Arbitre
|
||||
Genre.Homme=Homme
|
||||
Genre.Femme=Femme
|
||||
Genre.NA=Non défini
|
||||
Contact.COURRIEL=Courriel
|
||||
Contact.TELEPHONE=Téléphone
|
||||
Contact.SITE=Site web
|
||||
Contact.FACEBOOK=Facebook
|
||||
Contact.INSTAGRAM=Instagram
|
||||
Contact.AUTRE=Autre
|
||||
service.momentanement.indisponible=Service momentanément indisponible
|
||||
asso.introuvable=Association introuvable
|
||||
erreur.lors.calcul.du.trie=Erreur lors du calcul du tri
|
||||
page.out.of.range=Page out of range
|
||||
le.membre.appartient.pas.a.votre.club=Le membre n°%d n?appartient pas à votre club
|
||||
email.deja.utilise.par=L?adresse e-mail '%s' est déjà utilisée par %s %s
|
||||
try.edit.licence=Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (Tentative de modification non autorisée du nom sur la licence %d pour %s %s)
|
||||
email.deja.utilise=Adresse e-mail déjà utilisée
|
||||
regiter.new.membre=Pour enregistrer un nouveau membre, veuillez utiliser le bouton prévu à cet effet.
|
||||
permission.insuffisante=Permissions insuffisantes
|
||||
membre.deja.existent=Membre déjà existant
|
||||
membre.rm.err1=Impossible de supprimer un membre qui possède déjà un numéro de licence
|
||||
membre.rm.err2=Impossible de supprimer un membre ayant des licences actives
|
||||
pas.de.licence.pour.la.saison.en.cours=Aucune licence pour la saison en cours
|
||||
club.non.trouve=Club introuvable
|
||||
pas.d.affiliation.pour.la.saison.en.cours=Aucune affiliation pour la saison en cours
|
||||
erreur.lors.de.la.selection.des.membres=Erreur lors de la sélection des membres
|
||||
licence.rm.err1=Impossible de supprimer une licence pour laquelle un paiement est en cours
|
||||
licence.deja.demandee=Licence déjà demandée
|
||||
impossible.de.supprimer.une.licence.deja.validee=Impossible de supprimer une licence déjà validée
|
||||
impossible.de.supprimer.une.licence.deja.payee=Impossible de supprimer une licence déjà payée
|
||||
vous.ne.pouvez.pas.creer.de.competition=Vous n?êtes pas autorisé à créer une compétition
|
||||
user.not.found=Utilisateur %s introuvable
|
||||
inscription.fermee=Inscription fermée
|
||||
insc.err1=Vous n?êtes pas autorisé à inscrire ce membre (décision de l?administrateur de la compétition)
|
||||
insc.err2=Vous n?êtes pas autorisé à vous inscrire (décision de l?administrateur de la compétition)
|
||||
insc.err3=Modification bloquée par l?administrateur de la compétition
|
||||
licence.non.trouve=Licence %s introuvable
|
||||
nom.et.prenom.requis=Nom et prénom obligatoires
|
||||
combattant.non.trouve=Combattant %s %s introuvable
|
||||
le.membre.n.existe.pas=Le membre n°%d n?existe pas
|
||||
competition.is.not.internal=Competition is not INTERNAL
|
||||
erreur.de.format.des.contacts=Format des contacts invalide
|
||||
competition.not.found=Compétition introuvable
|
||||
saison.non.valid=Saison invalide
|
||||
demande.d.affiliation.deja.existante=Une demande d?affiliation existe déjà
|
||||
affiliation.deja.existante=Affiliation déjà existante
|
||||
licence.membre.n.1.inconnue=Licence du membre n°1 inconnue
|
||||
licence.membre.n.2.inconnue=Licence du membre n°2 inconnue
|
||||
licence.membre.n.3.inconnue=Licence du membre n°3 inconnue
|
||||
demande.d.affiliation.non.trouve=Demande d?affiliation introuvable
|
||||
160
src/main/webapp/package-lock.json
generated
160
src/main/webapp/package-lock.json
generated
@ -18,12 +18,16 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"i18next": "^25.7.4",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"obs-websocket-js": "^5.0.7",
|
||||
"proj4": "^2.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.5.2",
|
||||
"react-is": "^19.0.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
@ -345,12 +349,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@ -1871,6 +1872,14 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@ -2990,6 +2999,60 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.7.4",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
|
||||
"integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
|
||||
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
|
||||
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
|
||||
@ -3634,6 +3697,25 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
@ -4028,6 +4110,32 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
|
||||
"integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 25.6.2",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
|
||||
@ -4221,11 +4329,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||
@ -4704,6 +4807,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
@ -4852,6 +4960,14 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -4938,6 +5054,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@ -20,12 +20,16 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"i18next": "^25.7.4",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"obs-websocket-js": "^5.0.7",
|
||||
"proj4": "^2.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.5.2",
|
||||
"react-is": "^19.0.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import i18next from 'https://cdn.jsdelivr.net/npm/i18next@25.7.4/+esm';
|
||||
import i18nextHttpBackend from 'https://cdn.jsdelivr.net/npm/i18next-http-backend@3.0.2/+esm'
|
||||
import i18nextBrowserLanguagedetector from 'https://cdn.jsdelivr.net/npm/i18next-browser-languagedetector@8.2.0/+esm'
|
||||
|
||||
let apiUrlRoot = "";
|
||||
const rootDiv = document.getElementById("safca_api_data");
|
||||
|
||||
const header = `<h4>Résultat de la compétition :</h4>`
|
||||
const backButton = `<a onclick="setSubPage('home')" href="javascript:void(0);">Retour</a>`
|
||||
const cupImg = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
|
||||
style="width: 16px;" src="https://intra.ffsaf.fr/img/171891.png"
|
||||
alt="">`
|
||||
|
||||
|
||||
const voidFunction = () => {
|
||||
}
|
||||
let lastRf = 0;
|
||||
@ -41,18 +43,23 @@ function setSubPage(name) {
|
||||
}
|
||||
|
||||
function homePage() {
|
||||
rootDiv.innerHTML = header;
|
||||
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4>`;
|
||||
|
||||
let content = document.createElement('div');
|
||||
content.innerHTML = `
|
||||
<ul>
|
||||
<li><a onclick="setSubPage('poule')" href="javascript:void(0);">Par catégorie</a></li>
|
||||
<li><a onclick="setSubPage('comb')" href="javascript:void(0);">Par combattant</a></li>
|
||||
<li><a onclick="setSubPage('club')" href="javascript:void(0);">Par club</a></li>
|
||||
<li><a onclick="setSubPage('all')" href="javascript:void(0);">Tous les combattants</a></li>
|
||||
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li>
|
||||
<li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li>
|
||||
<li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li>
|
||||
<li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
|
||||
</ul>
|
||||
`
|
||||
rootDiv.append(content)
|
||||
|
||||
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
|
||||
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
|
||||
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
|
||||
document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
|
||||
}
|
||||
|
||||
let loadingAnimationStep = 0;
|
||||
@ -63,7 +70,7 @@ function startLoading(root) {
|
||||
element.id = id;
|
||||
|
||||
const anim = setInterval(() => {
|
||||
let str = "Chargement";
|
||||
let str = i18next.t('chargement');
|
||||
for (let i = 0; i < loadingAnimationStep; i++) {
|
||||
str += ".";
|
||||
}
|
||||
@ -84,11 +91,11 @@ function scoreToString(score) {
|
||||
const scorePrint = (s1) => {
|
||||
switch (s1) {
|
||||
case -997:
|
||||
return "disc.";
|
||||
return i18next.t('disc.');
|
||||
case -998:
|
||||
return "abs.";
|
||||
return i18next.t('abs.');
|
||||
case -999:
|
||||
return "for.";
|
||||
return i18next.t('for.');
|
||||
case -1000:
|
||||
return "";
|
||||
default:
|
||||
@ -111,11 +118,11 @@ function dateToString(date) {
|
||||
|
||||
let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24));
|
||||
if (d === 0)
|
||||
return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
return i18next.t('aujourdhuià', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})});
|
||||
else if (d === 1)
|
||||
return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
return i18next.t('hierà', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})});
|
||||
else if (d === 2)
|
||||
return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
return i18next.t('avanthierà', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})});
|
||||
else
|
||||
return date_.toLocaleDateString();
|
||||
}
|
||||
@ -175,11 +182,11 @@ function buildPouleMenu(isPoule, change_view) {
|
||||
return li;
|
||||
}
|
||||
|
||||
const li1 = createTab('Poule', isPoule, function () {
|
||||
const li1 = createTab(i18next.t('poule'), isPoule, function () {
|
||||
change_view(true);
|
||||
});
|
||||
ul.appendChild(li1);
|
||||
const li2 = createTab('Tournois', !isPoule, function () {
|
||||
const li2 = createTab(i18next.t('tournois'), !isPoule, function () {
|
||||
change_view(false);
|
||||
});
|
||||
ul.appendChild(li2);
|
||||
@ -195,12 +202,12 @@ function buildMatchArray(matchs) {
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 800px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Rouge</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('rouge')}</th>
|
||||
<th class="has-text-align-center" data-align="center"></th>
|
||||
<th class="has-text-align-center" data-align="center">Scores</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
|
||||
<th class="has-text-align-center" data-align="center"></th>
|
||||
<th class="has-text-align-center" data-align="center">Bleu</th>
|
||||
<th class="has-text-align-center" data-align="center">Date</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('bleu')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('date')}</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const match of matchs) {
|
||||
@ -225,12 +232,12 @@ function buildRankArray(rankArray) {
|
||||
<table style="width: 600px;overflow: auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Place</th>
|
||||
<th class="has-text-align-center" data-align="center">Nom</th>
|
||||
<th class="has-text-align-center" data-align="center">Victoire</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio</th>
|
||||
<th class="has-text-align-center" data-align="center">Points marqués</th>
|
||||
<th class="has-text-align-center" data-align="center">Points reçus</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('place')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('victoire')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratio')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsReçus')}</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const row of rankArray) {
|
||||
@ -255,10 +262,12 @@ function buildTree(treeData) {
|
||||
}
|
||||
|
||||
function poulePage(location) {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
|
||||
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
content.innerHTML = '<h4>Recherche par catégorie</h4>';
|
||||
content.innerHTML = `<h4>${i18next.t('rechercheParCatégorie')}</h4>`;
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
@ -286,7 +295,7 @@ function poulePage(location) {
|
||||
for (const g in poule.matchs) {
|
||||
if (Object.keys(poule.matchs).length > 1) {
|
||||
const text = document.createElement('h4');
|
||||
text.textContent = `Groupe ${g}`;
|
||||
text.textContent = `${i18next.t('poule')} ${g}`;
|
||||
text.style.marginTop = '2em';
|
||||
dataContainer.append(text);
|
||||
}
|
||||
@ -303,7 +312,7 @@ function poulePage(location) {
|
||||
for (const g in poule.matchs) {
|
||||
if (Object.keys(poule.matchs).length > 1) {
|
||||
const text = document.createElement('h4');
|
||||
text.textContent = `Groupe ${g}`;
|
||||
text.textContent = `${i18next.t('poule')} ${g}`;
|
||||
text.style.marginTop = '2em';
|
||||
dataContainer.append(text);
|
||||
}
|
||||
@ -323,7 +332,7 @@ function poulePage(location) {
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
dataContainer.replaceChildren(new Text("Erreur de chargement de la poule"));
|
||||
dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaPoule')));
|
||||
})
|
||||
.finally(() => stopLoading(loading));
|
||||
}
|
||||
@ -334,7 +343,7 @@ function poulePage(location) {
|
||||
.then(poule => {
|
||||
const select = document.createElement('select');
|
||||
select.setAttribute('id', poule.id);
|
||||
select.innerHTML = `<option value="0">--Sélectionner une catégorie--</option>`;
|
||||
select.innerHTML = `<option value="0">${i18next.t('--sélectionnerUneCatégorie--')}</option>`;
|
||||
for (const pouleKey of Object.keys(poule).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))) {
|
||||
select.innerHTML += `<option value="${poule[pouleKey]}">${pouleKey}</option>`;
|
||||
}
|
||||
@ -356,7 +365,7 @@ function poulePage(location) {
|
||||
loadPoule();
|
||||
}
|
||||
})
|
||||
.catch(() => rootDiv.append(new Text("Erreur de chargement des catégories")))
|
||||
.catch(() => rootDiv.append(new Text(i18next.t('erreurDeChargementDesCatégories'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
rfFonction = () => {
|
||||
@ -372,28 +381,33 @@ function buildCombView(comb) {
|
||||
let arrayContent = `
|
||||
<h3>Info :</h3>
|
||||
<ul>
|
||||
<li>Nom Prénom : ${comb.name}</li>
|
||||
<li>Club : ${comb.club}</li>
|
||||
<li>Catégorie : ${comb.cat}</li>
|
||||
<li>${i18next.t('nomPrénom')} : ${comb.name}</li>
|
||||
<li>${i18next.t('club')} : ${comb.club}</li>
|
||||
<li>${i18next.t('catégorie')} : ${comb.cat}</li>
|
||||
</ul>
|
||||
<h3>Statistique :</h3>
|
||||
<h3>${i18next.t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})</li>
|
||||
<li>Points marqués : ${comb.pointMake}</li>
|
||||
<li>Points reçus : ${comb.pointTake}</li>
|
||||
<li>Ratio du score (point marqué / point reçu): ${comb.pointRatio.toFixed(3)}</li>
|
||||
<li>${i18next.t('tauxDeVictoire2', {
|
||||
nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0),
|
||||
victoires: comb.totalWin,
|
||||
matchs: comb.matchs.length
|
||||
})}
|
||||
</li>
|
||||
<li>${i18next.t('pointsMarqués2', {nb: comb.pointMake})}</li>
|
||||
<li>${i18next.t('pointsReçus2', {nb: comb.pointTake})}</li>
|
||||
<li>${i18next.t('ratioDuScore2', {nb: comb.pointRatio.toFixed(3)})}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des matchs:</h3>
|
||||
<h3>${i18next.t('listeDesMatchs')}:</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 700px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Date</th>
|
||||
<th class="has-text-align-center" data-align="center">Poule</th>
|
||||
<th class="has-text-align-center" data-align="center">Adversaire</th>
|
||||
<th class="has-text-align-center" data-align="center">Scores</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('date')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('poule')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('adversaire')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratio')}</th>
|
||||
<th class="has-text-align-center" data-align="center"></th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
@ -414,10 +428,12 @@ function buildCombView(comb) {
|
||||
}
|
||||
|
||||
function combPage(location) {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
|
||||
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
content.innerHTML = '<h4>Recherche par combattant</h4>';
|
||||
content.innerHTML = `<h4>${i18next.t('rechercheParCombattant')}</h4>`;
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
@ -430,7 +446,7 @@ function combPage(location) {
|
||||
console.log(comb);
|
||||
dataContainer.replaceChildren(buildCombView(comb));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant")))
|
||||
.catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDuCombattant'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
}
|
||||
|
||||
@ -439,7 +455,7 @@ function combPage(location) {
|
||||
.then(response => response.json())
|
||||
.then(combs => {
|
||||
const select = document.createElement('select');
|
||||
select.innerHTML = `<option value="0">--Sélectionner un combattant--</option>`;
|
||||
select.innerHTML = `<option value="0">${i18next.t('--sélectionnerUnCombattant--')}</option>`;
|
||||
for (const comb of Object.keys(combs).sort()) {
|
||||
select.innerHTML += `<option value="${combs[comb]}">${comb}</option>`;
|
||||
}
|
||||
@ -458,7 +474,7 @@ function combPage(location) {
|
||||
loadComb(tmp);
|
||||
}
|
||||
})
|
||||
.catch(() => rootDiv.append(new Text("Erreur de chargement des combattants")))
|
||||
.catch(() => rootDiv.append(new Text(i18next.t('erreurDeChargementDesCombattants'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
rootDiv.append(content)
|
||||
@ -467,34 +483,34 @@ function combPage(location) {
|
||||
function buildClubView(club) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<h3>Info :</h3>
|
||||
<h3>${i18next.t('info')} :</h3>
|
||||
<ul>
|
||||
<li>Nom : ${club.name}</li>
|
||||
<li>Nombre d'inscris : ${club.nb_insc}</li>
|
||||
<li>${i18next.t('nom')} : ${club.name}</li>
|
||||
<li>${i18next.t('nombreDinscris')} : ${club.nb_insc}</li>
|
||||
</ul>
|
||||
<h3>Statistique :</h3>
|
||||
<h3>${i18next.t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>Nombre de match disputé : ${club.nb_match}</li>
|
||||
<li>Nombre de victoires : ${club.match_w}</li>
|
||||
<li>Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}</li>
|
||||
<li>Points marqués : ${club.pointMake}</li>
|
||||
<li>Points reçus : ${club.pointTake}</li>
|
||||
<li>Ratio de points moyen : ${club.ratioPoint.toFixed(3)}</li>
|
||||
<li>${i18next.t('nombreDeMatchDisputé2', {nb: club.nb_match})}</li>
|
||||
<li>${i18next.t('nombreDeVictoires2', {nb: club.match_w})}</li>
|
||||
<li>${i18next.t('ratioDeVictoiresMoyen2', {nb: club.ratioVictoire.toFixed(3)})}</li>
|
||||
<li>${i18next.t('pointsMarqués2', {nb: club.pointMake})}</li>
|
||||
<li>${i18next.t('pointsReçus2', {nb: club.pointTake})}</li>
|
||||
<li>${i18next.t('ratioDePointsMoyen2', {nb: club.ratioPoint.toFixed(3)})}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des menbres :</h3>
|
||||
<h3>${i18next.t('listeDesMenbres')} :</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 800px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Catégorie</th>
|
||||
<th class="has-text-align-center" data-align="center">Nom</th>
|
||||
<th class="has-text-align-center" data-align="center">Victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Défaites</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Points marqués</th>
|
||||
<th class="has-text-align-center" data-align="center">Points reçus</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio points</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('catégorie')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('victoires')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('défaites')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratioVictoires')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsReçus')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratioPoints')}</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const comb of club.combs) {
|
||||
@ -516,10 +532,12 @@ function buildClubView(club) {
|
||||
}
|
||||
|
||||
function clubPage(location) {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
|
||||
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
content.innerHTML = '<h4>Recherche par club</h4>';
|
||||
content.innerHTML = `<h4>${i18next.t('rechercheParClub')}</h4>`;
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
@ -532,7 +550,7 @@ function clubPage(location) {
|
||||
console.log(club);
|
||||
dataContainer.replaceChildren(buildClubView(club));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club")))
|
||||
.catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDuClub'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
}
|
||||
|
||||
@ -541,7 +559,7 @@ function clubPage(location) {
|
||||
.then(response => response.json())
|
||||
.then(clubs => {
|
||||
const select = document.createElement('select');
|
||||
select.innerHTML = `<option value="0">--Sélectionner un club--</option>`;
|
||||
select.innerHTML = `<option value="0">${i18next.t('--sélectionnerUnClub--')}</option>`;
|
||||
for (const club of Object.keys(clubs).sort()) {
|
||||
select.innerHTML += `<option value="${clubs[club]}">${club}</option>`;
|
||||
}
|
||||
@ -562,7 +580,7 @@ function clubPage(location) {
|
||||
loadComb(tmp);
|
||||
}
|
||||
})
|
||||
.catch(() => rootDiv.append(new Text("Erreur de chargement des clubs")))
|
||||
.catch(() => rootDiv.append(new Text(i18next.t('erreurDeChargementDesClubs'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
rootDiv.append(content)
|
||||
@ -571,27 +589,27 @@ function clubPage(location) {
|
||||
function buildCombsView(combs) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<h3>Statistique :</h3>
|
||||
<h3>${i18next.t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>Nombre d'inscris : ${combs.nb_insc}</li>
|
||||
<li>Nombre de match disputé : ${combs.tt_match}</li>
|
||||
<li>Points marqués : ${combs.point}</li>
|
||||
<li>${i18next.t('nombreDinscris2', {nb: combs.nb_insc})}</li>
|
||||
<li>${i18next.t('nombreDeMatchDisputé2', {nb: combs.tt_match})}</li>
|
||||
<li>${i18next.t('pointsMarqués2', {nb: combs.point})}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des combattants :</h3>
|
||||
<h3>${i18next.t('listeDesCombattants')} :</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 1200px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Catégorie</th>
|
||||
<th class="has-text-align-center" data-align="center">Club</th>
|
||||
<th class="has-text-align-center" data-align="center">Nom</th>
|
||||
<th class="has-text-align-center" data-align="center">Victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Défaites</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Points marqués</th>
|
||||
<th class="has-text-align-center" data-align="center">Points reçus</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratios points</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('catégorie')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('club')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('victoires')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('défaites')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratioVictoires')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsReçus')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratiosPoints')}</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const comb of combs.combs) {
|
||||
@ -614,7 +632,9 @@ function buildCombsView(combs) {
|
||||
}
|
||||
|
||||
function combsPage() {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
|
||||
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
|
||||
@ -628,26 +648,44 @@ function combsPage() {
|
||||
console.log(combs);
|
||||
dataContainer.replaceChildren(buildCombsView(combs));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste")))
|
||||
.catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaListe'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
content.append(dataContainer);
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
let path = document.getElementById('safca_api_script').src;
|
||||
const urlParams = new URLSearchParams(new URL(path).search);
|
||||
apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id");
|
||||
export async function initCompetitionApi(apiUrlRoot_) {
|
||||
apiUrlRoot = apiUrlRoot_;
|
||||
|
||||
const options = {
|
||||
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
|
||||
caches: [],
|
||||
}
|
||||
|
||||
await i18next
|
||||
.use(i18nextHttpBackend)
|
||||
.use(i18nextBrowserLanguagedetector)
|
||||
.init({
|
||||
fallbackLng: 'fr',
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: true,
|
||||
},
|
||||
detection: options,
|
||||
ns: ['result'],
|
||||
defaultNS: 'result',
|
||||
})
|
||||
|
||||
console.log("apiUrlRoot:", apiUrlRoot)
|
||||
console.log("FFSAF Competition API initialized.")
|
||||
|
||||
let hash = window.location.hash.substring(1);
|
||||
if (hash.length === 0)
|
||||
setSubPage('home');
|
||||
else
|
||||
setSubPage(hash);
|
||||
});
|
||||
}
|
||||
|
||||
class TreeNode {
|
||||
constructor(data) {
|
||||
@ -671,7 +709,7 @@ class TreeNode {
|
||||
}
|
||||
|
||||
function initTree(data_in) {
|
||||
out = [];
|
||||
let out = [];
|
||||
for (const din of data_in) {
|
||||
out.push(parseTree(din));
|
||||
}
|
||||
|
||||
143
src/main/webapp/public/locales/en/cm.json
Normal file
143
src/main/webapp/public/locales/en/cm.json
Normal file
@ -0,0 +1,143 @@
|
||||
{
|
||||
"--SélectionnerUnCombattant--": "-- Select a fighter --",
|
||||
"--Tous--": "-- All --",
|
||||
"actuel": "Current",
|
||||
"administration": "Administration",
|
||||
"adresseDuServeur": "Server address",
|
||||
"ajouter": "Add",
|
||||
"ajouterDesCombattants": "Add fighters",
|
||||
"attention": "Warning",
|
||||
"aucuneConfigurationObs": "No OBS configuration found, please import one",
|
||||
"bleu": "Blue",
|
||||
"blue": "Blue",
|
||||
"catégorie": "Category",
|
||||
"chrono.+/-...S": "+/- ... s",
|
||||
"chrono.+10S": "+10 s",
|
||||
"chrono.+1S": "+1 s",
|
||||
"chrono.-10S": "-10 s",
|
||||
"chrono.-1S": "-1 s",
|
||||
"chrono.arrêter": "Stop",
|
||||
"chrono.définirLeTemps": "Set time",
|
||||
"chrono.démarrer": "Start",
|
||||
"chrono.editionTemps": "Time editing",
|
||||
"chrono.entrezLeTempsEnS": "Enter time in seconds",
|
||||
"chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}",
|
||||
"chronomètre": "Stopwatch",
|
||||
"club": "Club",
|
||||
"compétition": "Competition",
|
||||
"compétitionManager": "Competition manager",
|
||||
"config.obs.dossierDesResources": "Resources folder",
|
||||
"config.obs.motDePasseDuServeur": "Server password",
|
||||
"config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition",
|
||||
"config.obs.ws": "ws://",
|
||||
"configurationObs": "OBS Configuration",
|
||||
"confirm1": "This match already has results; are you sure you want to delete it?",
|
||||
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
|
||||
"confirm2.title": "Tournament tree change",
|
||||
"confirm3.msg": "Do you really want to remove the {{typeStr}} part from the category? This will delete the matches contained in this part!",
|
||||
"confirm3.title": "Change category type",
|
||||
"confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!",
|
||||
"confirm4.title": "Delete category",
|
||||
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
|
||||
"contre": "vs",
|
||||
"créerLesMatchs": "Create matches",
|
||||
"demi-finalesEtFinales": "Semi-finals and finals",
|
||||
"duréePause": "Pause duration",
|
||||
"duréeRound": "Round duration",
|
||||
"editionDeLaCatégorie": "Edit category",
|
||||
"enregister": "Save",
|
||||
"enregistrer": "Save",
|
||||
"epéeBouclier": "Sword and shield",
|
||||
"err1": "A fighter cannot fight themselves!",
|
||||
"err2": "The combat zone name format is invalid. Please separate names with ';'.",
|
||||
"err3": "At least one type (pool or tournament) must be selected.",
|
||||
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
|
||||
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
|
||||
"exporter": "Export",
|
||||
"fermer": "Close",
|
||||
"finalesUniquement": "Finals only",
|
||||
"genre": "Gender",
|
||||
"genre.f": "F",
|
||||
"genre.h": "M",
|
||||
"genre.na": "NA",
|
||||
"inscrit": "Registered",
|
||||
"manche": "Round",
|
||||
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
|
||||
"matches": "Matches",
|
||||
"modifier": "Edit",
|
||||
"msg1": "There are already matches in this pool; what do you want to do with them?",
|
||||
"neRienConserver": "Keep nothing",
|
||||
"no": "No.",
|
||||
"nom": "Name",
|
||||
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
|
||||
"nouvelle...": "New...",
|
||||
"obs.préfixDesSources": "Source prefix",
|
||||
"pays": "Country",
|
||||
"poids": "Weight",
|
||||
"poule": "Pool",
|
||||
"poulePour": "Pool for: ",
|
||||
"préparation...": "Preparing...",
|
||||
"rouge": "Red",
|
||||
"réinitialiser": "Reset",
|
||||
"résultat": "Result",
|
||||
"sansPoule": "No pool",
|
||||
"sauvegarde": "Backup",
|
||||
"sauvegarder": "Save",
|
||||
"score": "Score",
|
||||
"score.err1": "Cannot end a tournament match in a draw.",
|
||||
"score.spe": "Special scores: <br/>-997: disqualified <br/>-998: absent <br/>-999: forfeit",
|
||||
"scores": "Scores",
|
||||
"secrétariatsDeLice": "Ring secretariats",
|
||||
"select.aucunCombattantDisponible": "No fighters available",
|
||||
"select.aucunCombattantSélectionné": "No fighters selected",
|
||||
"select.msg1": "(0 = disabled)",
|
||||
"select.recherche": "Search",
|
||||
"select.sélectionnerDesCombatants": "Select fighters",
|
||||
"select.à": "to",
|
||||
"serveur": "Server",
|
||||
"suivant": "Next",
|
||||
"supprimer": "Delete",
|
||||
"sélectionneLesModesDaffichage": "Select display modes",
|
||||
"sélectionner": "Select",
|
||||
"terminé": "Finished",
|
||||
"texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.",
|
||||
"toast.createCategory.error": "Error while creating the category",
|
||||
"toast.createCategory.pending": "Creating category...",
|
||||
"toast.createCategory.success": "Category created!",
|
||||
"toast.deleteCategory.error": "Error while deleting the category",
|
||||
"toast.deleteCategory.pending": "Deleting category...",
|
||||
"toast.deleteCategory.success": "Category deleted!",
|
||||
"toast.matchs.create.error": "Error while creating matches.",
|
||||
"toast.matchs.create.pending": "Creating matches in progress...",
|
||||
"toast.matchs.create.success": "Matches created successfully.",
|
||||
"toast.updateCategory.error": "Error while updating the category",
|
||||
"toast.updateCategory.pending": "Updating category...",
|
||||
"toast.updateCategory.success": "Category updated!",
|
||||
"toast.updateMatchScore.error": "Error while updating match score",
|
||||
"toast.updateMatchScore.pending": "Updating match score...",
|
||||
"toast.updateMatchScore.success": "Match score updated!",
|
||||
"toast.updateTrees.error": "Error while updating trees",
|
||||
"toast.updateTrees.init.error": "Error while creating trees",
|
||||
"toast.updateTrees.init.pending": "Creating tournament trees...",
|
||||
"toast.updateTrees.init.success": "Trees created!",
|
||||
"toast.updateTrees.pending": "Updating tournament trees...",
|
||||
"toast.updateTrees.success": "Trees updated!",
|
||||
"tournoi": "Tournament",
|
||||
"tournois": "Tournaments",
|
||||
"tousLesMatchs": "All matches",
|
||||
"toutConserver": "Keep all",
|
||||
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
|
||||
"ttm.admin.scripte": "Copy integration script",
|
||||
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",
|
||||
"ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration",
|
||||
"ttm.table.pub_aff": "Open public display",
|
||||
"ttm.table.pub_score": "Show scores on public display",
|
||||
"type": "Type",
|
||||
"téléchargement": "Download",
|
||||
"téléchargementEnCours": "Downloading...",
|
||||
"téléchargementTerminé!": "Download completed!",
|
||||
"uneCatégorie": "a category",
|
||||
"valider": "Validate",
|
||||
"zone": "Zone",
|
||||
"zoneDeCombat": "Combat zone"
|
||||
}
|
||||
543
src/main/webapp/public/locales/en/common.json
Normal file
543
src/main/webapp/public/locales/en/common.json
Normal file
@ -0,0 +1,543 @@
|
||||
{
|
||||
"(optionnelle)": "(optional)",
|
||||
"---SansClub---": "--- no club ---",
|
||||
"---ToutLesClubs---": "--- all clubs ---",
|
||||
"---ToutLesPays---": "--- all countries ---",
|
||||
"---TouteLesCatégories---": "--- all categories ---",
|
||||
"--NonLicencier--": "-- Not licensed --",
|
||||
"--SélectionnerCatégorie--": "-- Select category --",
|
||||
"1Catégorie": "+1 category",
|
||||
"2Catégorie": "+2 categories",
|
||||
"activer": "Activate",
|
||||
"admin": "Administration",
|
||||
"administrateur": "Administrator",
|
||||
"adresse": "Address",
|
||||
"adresseAdministrative": "Administrative address",
|
||||
"aff.ancienNom": "Former name: {{name}}",
|
||||
"aff.byMembreSim": "By similar member",
|
||||
"aff.byNewMenbre": "By new member",
|
||||
"aff.byNoLicence": "By license number",
|
||||
"aff.info1": "This club has already been affiliated (affiliation no. {{no}})",
|
||||
"aff.membreNo": "Member no. {{no}}",
|
||||
"aff.nomDuClub": "Club name",
|
||||
"aff.raisonDuRefus": "Reason for refusal",
|
||||
"aff.raisonDuRefus.msg": "Please indicate the reason for refusal",
|
||||
"aff.refusConfirm": "Are you sure you want to refuse this request?",
|
||||
"aff.refuserLaDemande": "Refuse the request",
|
||||
"aff.refuserLaDemande.detail": "Are you sure you want to refuse this request?",
|
||||
"aff.submit.error1": "Please enter a valid license number for member {{id}}",
|
||||
"aff.submit.error2": "Please enter a valid license number for member {{id}}",
|
||||
"aff.submit.error3": "Please enter a valid email for member {{id}}",
|
||||
"aff.toast.accept.error": "Failed to accept affiliation",
|
||||
"aff.toast.accept.pending": "Accepting affiliation in progress",
|
||||
"aff.toast.accept.success": "Affiliation accepted successfully 🎉",
|
||||
"aff.toast.del.error": "Failed to delete affiliation request",
|
||||
"aff.toast.del.pending": "Deleting affiliation request in progress",
|
||||
"aff.toast.del.success": "Affiliation request deleted successfully 🎉",
|
||||
"aff.toast.del2.error": "Failed to delete affiliation",
|
||||
"aff.toast.del2.pending": "Deleting affiliation in progress",
|
||||
"aff.toast.del2.success": "Affiliation deleted successfully 🎉",
|
||||
"aff.toast.save.error": "Failed to save affiliation request",
|
||||
"aff.toast.save.pending": "Saving affiliation request in progress",
|
||||
"aff.toast.save.success": "Affiliation request saved successfully 🎉",
|
||||
"aff.toast.save2.error": "Failed to save affiliation",
|
||||
"aff.toast.save2.pending": "Saving affiliation in progress",
|
||||
"aff.toast.save2.success": "Affiliation saved successfully 🎉",
|
||||
"aff_req.appuyerSurRechercher": "Press search to complete",
|
||||
"aff_req.association": "The association",
|
||||
"aff_req.button.cancel": "Cancel my request",
|
||||
"aff_req.button.confirm": "Confirm my affiliation request",
|
||||
"aff_req.button.save": "Save changes",
|
||||
"aff_req.denomination": "Denomination",
|
||||
"aff_req.disposeLicence": "Already has a license",
|
||||
"aff_req.error1": "The role of member {{i}} is required",
|
||||
"aff_req.error2": "The SIRET/RNA format is invalid",
|
||||
"aff_req.nomDeLassociation": "Association name",
|
||||
"aff_req.text1": "Affiliation is annual and valid for one sports season: from September 1st to August 31st of the following year.",
|
||||
"aff_req.text2": "To affiliate, a sports association must meet the following conditions:",
|
||||
"aff_req.text2.li": [
|
||||
"Have its headquarters in France or the Principality of Monaco",
|
||||
"Be constituted in accordance with Chapter 1 of Title II of Book 1 of the Sports Code",
|
||||
"Pursue a purpose that falls within the definition of Article 1 of the Federation's statutes",
|
||||
"Have statutes compatible with the principles of organization and operation of the Federation",
|
||||
"Ensure freedom of opinion and respect for the rights of defense within it, and prohibit any discrimination",
|
||||
"Comply with the rules of supervision, hygiene, and safety established by the Federation's regulations"
|
||||
],
|
||||
"aff_req.text3": "After validation of your request, you will receive a temporary identifier and password to access your FFSAF space",
|
||||
"aff_req.text4": "Note that to finalize your affiliation, you will need to:",
|
||||
"aff_req.text4.li1": "Have at least three licensed members, including the president",
|
||||
"aff_req.text4.li2": "Have paid the fees provided for by the federal regulations",
|
||||
"aff_req.text5": "You can later add publicly visible addresses for your training locations",
|
||||
"aff_req.text6": "Leave blank to make no changes. (If a coat of arms has already been sent with this request, it will be used; otherwise, we will use the one from the previous affiliation)",
|
||||
"aff_req.text7": "Affiliation request sent successfully",
|
||||
"aff_req.text8": "Once your request is validated, you will receive a temporary identifier and password to access your FFSAF space",
|
||||
"aff_req.toast.undo.error": "Failed to cancel affiliation request",
|
||||
"aff_req.toast.undo.pending": "Cancelling affiliation request in progress",
|
||||
"aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉",
|
||||
"afficherLétatDesAffiliation": "Display affiliation status",
|
||||
"affiliation": "Affiliation",
|
||||
"affiliationNo": "Affiliation no. {{no}}",
|
||||
"ajout": "Addition",
|
||||
"ajouterUnClub": "Add a club",
|
||||
"ajouterUnMembre": "Add a member",
|
||||
"all_season": "--- all seasons ---",
|
||||
"au": "to",
|
||||
"aucun": "None",
|
||||
"aucunMembreSélectionné": "No member selected",
|
||||
"back": "« back",
|
||||
"blason": "Coat of arms",
|
||||
"bureau": "Board",
|
||||
"button.accepter": "Accept",
|
||||
"button.ajouter": "Add",
|
||||
"button.annuler": "Cancel",
|
||||
"button.appliquer": "Apply",
|
||||
"button.confirmer": "Confirm",
|
||||
"button.créer": "Create",
|
||||
"button.enregister": "Save",
|
||||
"button.enregistrer": "Save",
|
||||
"button.fermer": "Close",
|
||||
"button.modifier": "Edit",
|
||||
"button.refuser": "Refuse",
|
||||
"button.seDésinscrire": "Unsubscribe",
|
||||
"button.suivant": "Next",
|
||||
"button.supprimer": "Delete",
|
||||
"cat.benjamin": "Benjamin",
|
||||
"cat.cadet": "Cadet",
|
||||
"cat.catégorieInconnue": "Unknown category",
|
||||
"cat.junior": "Junior",
|
||||
"cat.miniPoussin": "Mini Chick",
|
||||
"cat.minime": "Minime",
|
||||
"cat.poussin": "Chick",
|
||||
"cat.senior1": "Senior 1",
|
||||
"cat.senior2": "Senior 2",
|
||||
"cat.superMini": "Super Mini",
|
||||
"cat.vétéran1": "Veteran 1",
|
||||
"cat.vétéran2": "Veteran 2",
|
||||
"categorie": "category",
|
||||
"catégorie": "Category",
|
||||
"certificatMédical": "Medical certificate",
|
||||
"chargement...": "Loading...",
|
||||
"chargerLexcel": "Load Excel",
|
||||
"chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.",
|
||||
"choisir...": "Choose...",
|
||||
"club.aff_renew.msg": "Please select 0 to 3 board members to fill out the pre-request. (If a non-board member will become one next year, you can enter them in the next step)",
|
||||
"club.change.status": "To modify the above information, please contact the FFSAF by email.",
|
||||
"club.contact.tt": {
|
||||
"AUTRE": "Other club contact",
|
||||
"COURRIEL": "Club email address<br>Example: contact@ffsaf.fr",
|
||||
"FACEBOOK": "Club Facebook page starting with 'https://www.facebook.com/'<br>Example: https://www.facebook.com/ffmsf",
|
||||
"INSTAGRAM": "Club Instagram account starting with 'https://www.instagram.com/'<br>Example: https://www.instagram.com/ff_msf",
|
||||
"SITE": "Club website with or without 'https://'<br>Example: ffsaf.fr<br>Or https://ffsaf.fr",
|
||||
"TELEPHONE": "Club phone number<br>Example: 06 12 13 78 55"
|
||||
},
|
||||
"club.toast.aff.error": "Failed to load affiliations",
|
||||
"club.toast.aff.pending": "Loading affiliations in progress",
|
||||
"club.toast.aff.success": "Affiliations loaded successfully 🎉",
|
||||
"club.toast.del.error": "Failed to delete club",
|
||||
"club.toast.del.pending": "Deleting club in progress",
|
||||
"club.toast.del.success": "Club deleted successfully 🎉",
|
||||
"club.toast.new.error": "Failed to create club",
|
||||
"club.toast.new.pending": "Creating club in progress",
|
||||
"club.toast.new.success": "Club created successfully 🎉",
|
||||
"club.toast.save.error": "Failed to save club",
|
||||
"club.toast.save.pending": "Saving club in progress",
|
||||
"club.toast.save.success": "Club saved successfully 🎉",
|
||||
"clubExterne": "External club",
|
||||
"club_one": "Club",
|
||||
"club_other": "Clubs",
|
||||
"club_zero": "No club",
|
||||
"combattant": "fighter",
|
||||
"comp.aff.blason": "Display the club's coat of arms on screens",
|
||||
"comp.aff.flag": "Display the fighter's country on screens",
|
||||
"comp.ajoutRapide": "Quick add",
|
||||
"comp.ajouterUnCombattant": "Add a fighter",
|
||||
"comp.ajouterUnInvité": "Add a guest",
|
||||
"comp.billetterie": "Ticketing",
|
||||
"comp.billetterieHelloasso": "HelloAsso Ticketing",
|
||||
"comp.catégorieNormalisée": "Standardized category",
|
||||
"comp.combattantNonTrouvé": "Fighter not found",
|
||||
"comp.combattantsInscrits": "Registered fighters",
|
||||
"comp.compétitionFuture": "Future competition",
|
||||
"comp.compétitionPassée": "Past competition",
|
||||
"comp.créationCompétition": "Competition creation",
|
||||
"comp.dateDinscription": "Registration date",
|
||||
"comp.editionCompétition": "Competition edition",
|
||||
"comp.error1": "The end date must be after the start date.",
|
||||
"comp.error2": "Please enter the start and end dates of registration.",
|
||||
"comp.error3": "The end date of registration must be after the start date of registration.",
|
||||
"comp.exporterLesInscription": "Export registrations",
|
||||
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email for receiving failed registrations",
|
||||
"comp.ha.error1": "Please enter the HelloAsso ticketing URL and associated rates.",
|
||||
"comp.ha.error2": "The HelloAsso ticketing URL is invalid. Please check the URL format.",
|
||||
"comp.ha.error3": "Please enter the email for receiving failed registrations.",
|
||||
"comp.ha.tarifsHelloasso": "HelloAsso rates",
|
||||
"comp.ha.text1": "To ensure good interconnection with HelloAsso, please follow these instructions:",
|
||||
"comp.ha.text2": "<strong>Configure the notification URL:</strong> In order for us to receive a notification for each registration, it is necessary to configure the notification URL of your HelloAsso account to redirect to \"https://intra.ffsaf.fr/api/webhook/ha\". To do this, from the home page of your association on HelloAsso, go to <strong>My account</strong> > <strong>Settings</strong> > <strong>Integrations and API</strong> section Notification and copy-paste <strong>https://intra.ffsaf.fr/api/webhook/ha</strong> in the <strong>My callback URL</strong> field and save.",
|
||||
"comp.ha.text3": "<strong>Copy-paste the exact name of the rates</strong> <em>-separated by semicolons-</em> that will result in automatic registration. All these rates must imperatively require the license number as a mandatory field. To do this, during the configuration of your ticketing at step 3, click on <strong>+ Add information</strong>, enter the exact title <strong>License number</strong>, in <strong>Type of response desired</strong> enter Number, select the rates entered previously and make the information mandatory.",
|
||||
"comp.ha.text4": "<strong>Copy-paste the URL of your ticketing</strong> in the field below. It should look like this: https://www.helloasso.com/associations/__asso-name-on-helloasso__/events/__ticketing-name__",
|
||||
"comp.ha.text5": "HelloAsso ticketing URL",
|
||||
"comp.ha.text6": "If for any reason the automatic registration fails, an email will be sent to this address to inform you",
|
||||
"comp.informationsGénéralesSurLaCompétition": "General information about the competition",
|
||||
"comp.informationsSurLeModeDinscription": "Information on the registration mode",
|
||||
"comp.informationsTechniques": "Technical information",
|
||||
"comp.inscription": "Registration",
|
||||
"comp.inscriptionModeAdministrateur": "Registration - administrator mode",
|
||||
"comp.inscriptionsLibres": "Free registrations",
|
||||
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Registrations by competition administrators",
|
||||
"comp.inscriptionsParLesResponsablesDeClub": "Registrations by club managers",
|
||||
"comp.inscriptionsSurLaBilletterieHelloasso": "Registrations on the HelloAsso ticketing",
|
||||
"comp.modal.information": "Information",
|
||||
"comp.modal.poids": "Weight (in kg)",
|
||||
"comp.modal.recherche": "Search*",
|
||||
"comp.modal.surclassement": "Overclassification",
|
||||
"comp.modal.text1": "Guests are reserved for members not licensed by the federation. Fighters registered via this form will not be able to see their results from their profile.",
|
||||
"comp.modal.text2": "Prevent members/clubs from modifying this registration",
|
||||
"comp.modifierLesParticipants": "View/Edit participants",
|
||||
"comp.monInscription": "My registration",
|
||||
"comp.noDeLicence": "License number",
|
||||
"comp.nouvelleCompétition": "New competition",
|
||||
"comp.organisateur": "Organizer",
|
||||
"comp.quiPeutInscrire": "Who can register",
|
||||
"comp.reg.libres": "Free",
|
||||
"comp.reg.parLesAdministrateursDeLaCompétition": "By competition administrators",
|
||||
"comp.reg.parLesResponsablesDeClub": "By club managers",
|
||||
"comp.reg.surLaBilletterieHelloasso": "On the HelloAsso ticketing",
|
||||
"comp.responsablesEtBureauxDesAssociations": "Managers and boards of associations",
|
||||
"comp.sinscrire": "Register",
|
||||
"comp.supprimerLaCompétition": "Delete competition",
|
||||
"comp.supprimerLaCompétition.msg": "Are you sure you want to delete this competition and all associated results?",
|
||||
"comp.surclassement_one": "{{cat}} with 1 overclassification",
|
||||
"comp.surclassement_other": "{{cat}} with {{count}} overclassifications",
|
||||
"comp.surclassement_zero": "{{cat}}",
|
||||
"comp.text2": "Visible to the public (appears in the list of competitions)",
|
||||
"comp.text3": "If not checked, the competition will only be visible to people who can register participants.",
|
||||
"comp.tips": "Tip 1: It is possible to ban a fighter, which will prevent them from being re-registered by any means other than by an administrator of this competition. To do this, click on the small <1/> next to their name.<br/>Tip 2: It is also possible to lock the modifications of their registration from their file, which will prevent it from being modified/deleted by themselves and/or a club manager.",
|
||||
"comp.toast.del.error": "Failed to delete competition",
|
||||
"comp.toast.del.pending": "Deleting competition in progress",
|
||||
"comp.toast.del.success": "Competition deleted successfully 🎉",
|
||||
"comp.toast.params.error": "Failed to update competition parameters",
|
||||
"comp.toast.params.pending": "Updating competition parameters in progress...",
|
||||
"comp.toast.params.success": "Competition parameters updated successfully 🎉",
|
||||
"comp.toast.register.add.error": "Fighter not found",
|
||||
"comp.toast.register.add.pending": "Search in progress",
|
||||
"comp.toast.register.add.success": "Fighter found and added/updated",
|
||||
"comp.toast.register.ban.error": "Error",
|
||||
"comp.toast.register.ban.pending": "Unregistration in progress",
|
||||
"comp.toast.register.ban.success": "Fighter unregistered and banned",
|
||||
"comp.toast.register.del.error": "Error",
|
||||
"comp.toast.register.del.pending": "Unregistration in progress",
|
||||
"comp.toast.register.del.success": "Fighter unregistered",
|
||||
"comp.toast.register.self.add.error": "Error during registration",
|
||||
"comp.toast.register.self.add.pending": "Registration in progress",
|
||||
"comp.toast.register.self.add.success": "Registration completed 🎉",
|
||||
"comp.toast.register.self.del.error": "Error during unregistration",
|
||||
"comp.toast.register.self.del.pending": "Unregistration in progress",
|
||||
"comp.toast.register.self.del.success": "Unregistration completed",
|
||||
"comp.toast.save.error": "Failed to save competition",
|
||||
"comp.toast.save.pending": "Saving competition in progress",
|
||||
"comp.toast.save.success": "Competition saved successfully 🎉",
|
||||
"comp.tousLesMembresDeLaFfsaf": "All FFSAF members",
|
||||
"comp.typeDinscription": "Registration type",
|
||||
"comp.uniquementLesAdministrateursDeLaCompétition": "Only competition administrators",
|
||||
"comp.warn1": "Are you sure you want to unregister and ban this fighter from the competition?\n(You can re-register them later)",
|
||||
"comp.warn2": "Are you sure you want to unregister this fighter?\nThis will not unregister them from the HelloAsso ticketing and will not refund them.",
|
||||
"comp.warn3": "Are you sure you want to unregister this fighter?",
|
||||
"comp.warn4": "Are you sure you want to unregister yourself?",
|
||||
"comp_manage": "Competitions Manager",
|
||||
"competition_one": "Competition",
|
||||
"competition_other": "Competitions",
|
||||
"compte": "Account",
|
||||
"compétition": "Competition",
|
||||
"configuration": "Configuration",
|
||||
"conserverLancienEmail": "Keep the old email",
|
||||
"contactAdministratif": "Administrative contact",
|
||||
"contactInterne": "Internal contact",
|
||||
"contact_one": "Contact",
|
||||
"contact_other": "Contacts",
|
||||
"date": "Date",
|
||||
"dateDeNaissance": "Date of birth",
|
||||
"days": [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday"
|
||||
],
|
||||
"de": "of",
|
||||
"demandeDaffiliationEnCours": "Affiliation request in progress",
|
||||
"demandeDeLicence": "License request",
|
||||
"demander": "Request",
|
||||
"description": "Description",
|
||||
"dlAff": "Download affiliation certificate",
|
||||
"donnéesAdministratives": "Administrative data",
|
||||
"du": "From",
|
||||
"dun": "of a",
|
||||
"définirLidDuCompte": "Define account ID",
|
||||
"editionDeL'affiliation": "Editing affiliation",
|
||||
"editionDeLaDemande": "Editing request",
|
||||
"editionDeLaLicence": "Editing license",
|
||||
"editionDeLaSéléction": "Editing selection",
|
||||
"editionDeLadresse": "Editing address",
|
||||
"email": "Email",
|
||||
"en": "in",
|
||||
"enAttente": "Pending",
|
||||
"erreurDePaiement": "Payment error😕",
|
||||
"erreurDePaiement.detail": "Error message:",
|
||||
"erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.",
|
||||
"espaceAdministration": "Administration space",
|
||||
"f": "F",
|
||||
"faitPar": "Done by",
|
||||
"femme": "Female",
|
||||
"filtre": "Filter",
|
||||
"genre": "Gender",
|
||||
"gestionGroupée": "Group management",
|
||||
"gradeDarbitrage": "Refereeing grade",
|
||||
"h": "M",
|
||||
"home": {
|
||||
"header1": "For licensed members",
|
||||
"header2": "For clubs",
|
||||
"text1": "Here you will find all your information as well as the status of your registration with the federation. You can also download your registration certificate, register for competitions, and consult your results, provided that the organizing club has entered them.<br/><br/>During your first registration, you will receive an email containing your login information; this email will be sent once your license is validated by the secretariat.",
|
||||
"text2": "This is where you can take out federal licenses for your members, where you can request or renew your affiliation, enter your schedules, training locations, and social networks, which will then be displayed on the ffsaf.fr website.<br/>You will also have the possibility to publish registration forms for your competitions and to record the results.<br/><br/>Not yet affiliated with the federation? Click <1>here</1> to make your first request.",
|
||||
"welcome_message": "Welcome to the intranet of the Fédération France Soft Armored Fighting"
|
||||
},
|
||||
"homme": "Male",
|
||||
"horairesD'entraînements": "Training schedules",
|
||||
"information": "Information",
|
||||
"invité": "guest",
|
||||
"keepEmpty": "Leave blank to make no changes.",
|
||||
"le": "the",
|
||||
"licence": "License",
|
||||
"licenceNo": "License no. {{no}}",
|
||||
"lieu": "Place",
|
||||
"lieuxDentraînements": "Training locations",
|
||||
"loading": "Loading...",
|
||||
"me": {
|
||||
"result": {
|
||||
"PRIVATE": "Private (visible only to me)",
|
||||
"PUBLIC": "Public (visible to all)",
|
||||
"REGISTERED_ONLY": "Logged-in members (visible to federation members)",
|
||||
"REGISTERED_ONLY_NO_DETAILS": "Logged-in members - hide details (visible to federation members)"
|
||||
},
|
||||
"toast.settings.error": "Failed to update settings 😕",
|
||||
"toast.settings.pending": "Updating settings in progress...",
|
||||
"toast.settings.success": "Settings updated successfully 🎉"
|
||||
},
|
||||
"me.changerMonMotDePasse": "Change my password",
|
||||
"me.formationDarbitrage": "Refereeing training",
|
||||
"me.paramètresDuCompte": "Account settings",
|
||||
"me.rôleAuSienDuClub": "Role within the club",
|
||||
"me.visibilitéDesRésultats": "Visibility of results",
|
||||
"member_one": "Member",
|
||||
"member_other": "Members",
|
||||
"membre.emailVideàLaLigne": "Empty email on line {{no}}",
|
||||
"membre.emailVérifié": "Email verified",
|
||||
"membre.filtre.inactif": "Show inactive fighters",
|
||||
"membre.filtre.licence": "Show license status",
|
||||
"membre.filtre.licences": [
|
||||
"No request or valid license",
|
||||
"With request or valid license",
|
||||
"Request in progress",
|
||||
"License validated",
|
||||
"All license statuses",
|
||||
"Complete request",
|
||||
"Incomplete request"
|
||||
],
|
||||
"membre.filtre.payement": [
|
||||
"Without payment",
|
||||
"With payment",
|
||||
"All payment statuses"
|
||||
],
|
||||
"membre.identifiant": "Identifier",
|
||||
"membre.import.err1": "Invalid medical certificate date format on line {{no}}",
|
||||
"membre.import.err2": "Invalid medical certificate date format on line {{no}}",
|
||||
"membre.import.err3": "Empty date of birth on line {{no}}",
|
||||
"membre.import.err4": "Invalid date of birth format on line {{no}}",
|
||||
"membre.import.err5": "Invalid email on line {{no}}",
|
||||
"membre.import.errTT_one": "{{count}} error in the file, operation cancelled",
|
||||
"membre.import.errTT_other": "{{count}} errors in the file, operation cancelled",
|
||||
"membre.import.warn_one": "{{count}} medical certificate not filled",
|
||||
"membre.import.warn_other": "{{count}} medical certificates not filled",
|
||||
"membre.info.emailInfo": "The email is used to create an account to log in to the site and must be unique.<br/>For minors, the parents' email can be used multiple times using the following syntax: {'email.parent+<alphanumeric characters>@example.com'}.<br/>Examples: mail.parent+1@example.com, mail.parent+titouan@example.com, mail.parent+cedrique@example.com",
|
||||
"membre.info.error1": "Please select a valid country 😕",
|
||||
"membre.info.error2": "Please select a valid club 😕",
|
||||
"membre.initaccount": "Initialize account",
|
||||
"membre.initaccount.text1": "Enter the account UUID",
|
||||
"membre.initaccount.text2": "Warning: only change a member's ID if you are sure of what you are doing...",
|
||||
"membre.noAccount": "This member does not have an account...",
|
||||
"membre.noAccount.clubMsg": "An account will be created by the federation when their first license is validated",
|
||||
"membre.nomVideàLaLigne": "Empty last name on line {{no}}",
|
||||
"membre.prénomVideàLaLigne": "Empty first name on line {{no}}",
|
||||
"membre.toast.compte.created": "Account created successfully 🎉",
|
||||
"membre.toast.compte.error": "Failed to create account",
|
||||
"membre.toast.compte.pending": "Creating account in progress",
|
||||
"membre.toast.del.error": "Failed to delete account",
|
||||
"membre.toast.del.pending": "Deleting account in progress",
|
||||
"membre.toast.del.success": "Account deleted successfully 🎉",
|
||||
"membre.toast.id.error": "Failed to define identifier",
|
||||
"membre.toast.id.pending": "Defining identifier in progress",
|
||||
"membre.toast.id.success": "Identifier defined successfully 🎉",
|
||||
"membre.toast.licence.ask.del.error": "Failed to delete license request",
|
||||
"membre.toast.licence.ask.del.pending": "Deleting license request in progress",
|
||||
"membre.toast.licence.ask.del.success": "License request deleted successfully 🎉",
|
||||
"membre.toast.licence.ask.error": "Failed to request license",
|
||||
"membre.toast.licence.ask.pending": "Saving license request in progress",
|
||||
"membre.toast.licence.ask.success": "License request saved successfully 🎉",
|
||||
"membre.toast.licence.del.error": "Failed to delete license",
|
||||
"membre.toast.licence.del.pending": "Deleting license in progress",
|
||||
"membre.toast.licence.del.success": "License deleted successfully 🎉",
|
||||
"membre.toast.licence.save.error": "Failed to save license",
|
||||
"membre.toast.licence.save.pending": "Saving license in progress",
|
||||
"membre.toast.licence.save.success": "License saved successfully 🎉",
|
||||
"membre.toast.licences.export.error": "Failed to export licenses",
|
||||
"membre.toast.licences.export.pending": "Exporting licenses in progress",
|
||||
"membre.toast.licences.export.success": "Licenses exported successfully 🎉",
|
||||
"membre.toast.licences.import.error": "Failed to send changes",
|
||||
"membre.toast.licences.import.pending": "Sending changes in progress",
|
||||
"membre.toast.licences.import.success": "Changes sent successfully 🎉",
|
||||
"membre.toast.licences.load.error": "Failed to load licenses",
|
||||
"membre.toast.licences.load.pending": "Loading licenses in progress",
|
||||
"membre.toast.licences.load.success": "Licenses loaded successfully 🎉",
|
||||
"membre.toast.perm.error": "Failed to update permissions 😕",
|
||||
"membre.toast.perm.pending": "Updating permissions in progress...",
|
||||
"membre.toast.perm.success": "Permissions updated successfully 🎉",
|
||||
"membre.toast.save.error": "Failed to update profile 😕",
|
||||
"membre.toast.save.pending": "Updating profile in progress...",
|
||||
"membre.toast.save.success": "Profile updated successfully 🎉",
|
||||
"membre.toast.select.del.error": "Failed to delete selection",
|
||||
"membre.toast.select.del.pending": "Deleting selection in progress",
|
||||
"membre.toast.select.del.success": "Selection deleted successfully 🎉",
|
||||
"membre.toast.select.save.error": "Failed to save selection",
|
||||
"membre.toast.select.save.pending": "Saving selection in progress",
|
||||
"membre.toast.select.save.success": "Selection saved successfully 🎉",
|
||||
"mettreàJours": "Update",
|
||||
"modification": "Modification",
|
||||
"nationalité": "Nationality",
|
||||
"nav": {
|
||||
"account": "My account",
|
||||
"aff_request": "Affiliation request",
|
||||
"club": {
|
||||
"my": "My club"
|
||||
},
|
||||
"competitions": {
|
||||
"results": "My results"
|
||||
},
|
||||
"home": "Home",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"space": "My space",
|
||||
"title": "FFSAF Intranet"
|
||||
},
|
||||
"noLicence": "License no.",
|
||||
"noSiretOuRna": "SIRET or RNA no.",
|
||||
"nom": "Last name",
|
||||
"nombreDeLicences": "Number of licenses",
|
||||
"nombreDeLicencesParCatégorie": "Number of licenses by category for {{saison}}",
|
||||
"non": "No",
|
||||
"nonDéfinie": "Not defined",
|
||||
"nonValidée": "Not validated",
|
||||
"nouveauClub": "New club",
|
||||
"nouveauMembre": "New member",
|
||||
"nouvelEmail": "New email",
|
||||
"ou": "or",
|
||||
"oui": "Yes",
|
||||
"outdated_session": {
|
||||
"login_button": "Log in again",
|
||||
"message": "Your session has expired, please log in again to continue using the application.",
|
||||
"title": "Session expired"
|
||||
},
|
||||
"pageClub": "Club page",
|
||||
"pageMembre": "Member page",
|
||||
"pageNouveauClub": "New club page",
|
||||
"page_info_full": "Line {{line}} to {{tt_line}} (page {{page}} of {{tt_page}})",
|
||||
"page_info_ligne": "{{show}} line(s) displayed out of {{total}}",
|
||||
"paiementDeLaLicence": "License payment",
|
||||
"paiementDesLicences": "License payments",
|
||||
"par": "by",
|
||||
"pasDeLicence": "No license",
|
||||
"payment.ha.info": "HelloAsso's solidarity model guarantees that 100% of your payment will be transferred to the chosen association. You can support the help they provide to associations by leaving a voluntary contribution to HelloAsso at the time of your payment.",
|
||||
"payment.info": "About HelloAsso",
|
||||
"payment.paiementSécurisé": "Secure payment",
|
||||
"payment.payerAvec": "Pay with",
|
||||
"payment.recap": "{{count}} license(s) selected<br/>Total amount to pay: {{total}} €",
|
||||
"paymentDesLicences": "License payments",
|
||||
"paymentDesLicences.msg_one": "Are you sure you want to mark the license as paid?",
|
||||
"paymentDesLicences.msg_other": "Are you sure you want to mark the {{count}} licenses as paid?",
|
||||
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
|
||||
"paymentOk": "🎉Your payment has been processed successfully.🎉",
|
||||
"paymentOk.msg": "Thank you for your payment. The licenses should be activated within the next hour, provided that the medical certificate is completed.",
|
||||
"pays": "Country",
|
||||
"perm.administrateurDeLaFédération": "Federation administrator",
|
||||
"perm.créerDesCompétion": "Create competitions",
|
||||
"perm.ffsafIntra": "FFSAF intra",
|
||||
"permission": "Permission",
|
||||
"photos": "Photos",
|
||||
"prenom": "First name",
|
||||
"prénomEtNom": "First and last name",
|
||||
"rechercher": "Search",
|
||||
"rechercher...": "Search...",
|
||||
"registration_one": "Registration",
|
||||
"registration_other": "Registrations",
|
||||
"renouveler": "Renew",
|
||||
"renouvellementDeLaffiliation": "Affiliation renewal",
|
||||
"result_one": "Result",
|
||||
"result_other": "Results",
|
||||
"retouràLaListeDeMembres": "Back to member list",
|
||||
"role": "Role",
|
||||
"role.membre": "Member",
|
||||
"role.membreDuBureau": "Board member",
|
||||
"role.président": "President",
|
||||
"role.secrétaire": "Secretary",
|
||||
"role.trésorier": "Treasurer",
|
||||
"role.vise-président": "Vice-President",
|
||||
"role.vise-secrétaire": "Vice-Secretary",
|
||||
"role.vise-trésorier": "Vice-Treasurer",
|
||||
"saison": "Season",
|
||||
"secrétariatsDeLice": "Ring secretariats",
|
||||
"selectionner...": "Select...",
|
||||
"siretOuRna": "SIRET or RNA",
|
||||
"stats": "Statistics",
|
||||
"statue": "Statue",
|
||||
"status": "Status",
|
||||
"statuts": "Statutes",
|
||||
"supprimerLeClub": "Delete club",
|
||||
"supprimerLeClub.msg": "Are you sure you want to delete this club?",
|
||||
"supprimerLeCompte": "Delete account",
|
||||
"supprimerLeCompte.msg": "Are you sure you want to delete this account?",
|
||||
"sélectionEnéquipeDeFrance": "Selection in the French team",
|
||||
"sélectionner...": "Select...",
|
||||
"toast.edit.error": "Failed to save changes",
|
||||
"toast.edit.pending": "Saving changes in progress",
|
||||
"toast.edit.success": "Changes saved successfully 🎉",
|
||||
"toast.licence.bulk.pay.error": "Failed to mark licenses as paid",
|
||||
"toast.licence.bulk.pay.pending": "Marking licenses as paid in progress",
|
||||
"toast.licence.bulk.pay.success": "Licenses marked as paid successfully 🎉",
|
||||
"toast.licence.bulk.valid.error": "Failed to validate licenses",
|
||||
"toast.licence.bulk.valid.pending": "Validating licenses in progress",
|
||||
"toast.licence.bulk.valid.success": "Licenses validated successfully 🎉",
|
||||
"toast.licence.order.error": "Failed to create order",
|
||||
"toast.licence.order.pending": "Creating order in progress",
|
||||
"toast.licence.order.success": "Order created successfully 🎉",
|
||||
"trie": "Sort",
|
||||
"téléchargerLexcelDesMembres": "Download members' Excel",
|
||||
"téléchargerLexcelDesMembres.info": "To be used as a template to update information",
|
||||
"téléchargéeLaLicence": "Download license",
|
||||
"validationDeLaLicence": "License validation",
|
||||
"validationDesLicences": "License validations",
|
||||
"validerDesLicences": "Validate licenses",
|
||||
"validerLePayement_one": "Validate payment for the selected license",
|
||||
"validerLePayement_other": "Validate payment for the {{count}} selected licenses",
|
||||
"validerLePayement_zero": "$t(validerLePayement_other)",
|
||||
"validerLicence.msg_one": "Are you sure you want to validate the license?",
|
||||
"validerLicence.msg_other": "Are you sure you want to validate the {{count}} licenses?",
|
||||
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
|
||||
"validerLicence_one": "Validate the selected license",
|
||||
"validerLicence_other": "Validate the {{count}} selected licenses",
|
||||
"validerLicence_zero": "$t(validerLicence_other)",
|
||||
"validée": "Validated",
|
||||
"voir/modifierLesParticipants": "View/Edit participants",
|
||||
"voirLesStatues": "View statues",
|
||||
"à": "at",
|
||||
"étatDeLaDemande": "Request status"
|
||||
}
|
||||
67
src/main/webapp/public/locales/en/result.json
Normal file
67
src/main/webapp/public/locales/en/result.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"--sélectionnerUnClub--": "--Select a club--",
|
||||
"--sélectionnerUnCombattant--": "--Select a fighter--",
|
||||
"--sélectionnerUneCatégorie--": "--Select a category--",
|
||||
"abs.": "abs.",
|
||||
"adversaire": "Opponent",
|
||||
"aujourdhuià": "Today at {{time}}",
|
||||
"avanthierà": "Day before yesterday at {{time}}",
|
||||
"back": "« back",
|
||||
"bleu": "Blue",
|
||||
"catégorie": "Category",
|
||||
"chargement": "Loading",
|
||||
"club": "Club",
|
||||
"combattant": "Fighter",
|
||||
"combattants": "Fighters",
|
||||
"date": "Date",
|
||||
"disc.": "DQ",
|
||||
"défaites": "Losses",
|
||||
"erreurDeChargementDeLaListe": "Error loading the list",
|
||||
"erreurDeChargementDeLaPoule": "Error loading the pool",
|
||||
"erreurDeChargementDesCatégories": "Error loading categories",
|
||||
"erreurDeChargementDesClubs": "Error loading clubs",
|
||||
"erreurDeChargementDesCombattants": "Error loading fighters",
|
||||
"erreurDeChargementDuClub": "Error loading club",
|
||||
"erreurDeChargementDuCombattant": "Error loading fighter",
|
||||
"for.": "forf.",
|
||||
"hierà": "Yesterday at {{time}}",
|
||||
"info": "Info",
|
||||
"listeDesCombattants": "List of fighters",
|
||||
"listeDesMatchs": "List of matches",
|
||||
"listeDesMembres": "List of members",
|
||||
"listeDesMenbres": "List of members",
|
||||
"nom": "Last name",
|
||||
"nomPrénom": "Last name First name",
|
||||
"nombreDeMatchDisputé2": "Number of matches played: {{nb}}",
|
||||
"nombreDeVictoires2": "Number of wins: {{nb}}",
|
||||
"nombreDinscris": "Number of registered",
|
||||
"nombreDinscris2": "Number of registered: {{nb}}",
|
||||
"parCatégorie": "By category",
|
||||
"parClub": "By club",
|
||||
"parCombattant": "By fighter",
|
||||
"place": "Place",
|
||||
"pointsMarqués": "Points scored",
|
||||
"pointsMarqués2": "Points scored: {{nb}}",
|
||||
"pointsReçus": "Points received",
|
||||
"pointsReçus2": "Points received: {{nb}}",
|
||||
"poule": "Pool",
|
||||
"ratio": "Ratio",
|
||||
"ratioDePointsMoyen2": "Average points ratio: {{nb}}",
|
||||
"ratioDeVictoiresMoyen2": "Average win ratio: {{nb}}",
|
||||
"ratioDuScore2": "Score ratio (points scored / points received): {{nb}}",
|
||||
"ratioPoints": "Points ratio",
|
||||
"ratioVictoires": "Win ratio",
|
||||
"ratiosPoints": "Points ratios",
|
||||
"rechercheParCatégorie": "Search by category",
|
||||
"rechercheParClub": "Search by club",
|
||||
"rechercheParCombattant": "Search by fighter",
|
||||
"rouge": "Red",
|
||||
"résultatDeLaCompétition": "Competition result",
|
||||
"scores": "Scores",
|
||||
"statistique": "Statistics",
|
||||
"tauxDeVictoire2": "Win rate: {{nb}}% ({{victoires}} out of {{matchs}})",
|
||||
"tournois": "Tournaments",
|
||||
"tousLesCombattants": "All fighters",
|
||||
"victoire": "Win",
|
||||
"victoires": "Wins"
|
||||
}
|
||||
143
src/main/webapp/public/locales/fr/cm.json
Normal file
143
src/main/webapp/public/locales/fr/cm.json
Normal file
@ -0,0 +1,143 @@
|
||||
{
|
||||
"--SélectionnerUnCombattant--": "-- Sélectionner un combattant --",
|
||||
"--Tous--": "-- Tous --",
|
||||
"actuel": "Actuel",
|
||||
"administration": "Administration",
|
||||
"adresseDuServeur": "Adresse du serveur",
|
||||
"ajouter": "Ajouter",
|
||||
"ajouterDesCombattants": "Ajouter des combattants",
|
||||
"attention": "Attention",
|
||||
"aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une",
|
||||
"bleu": "Bleu",
|
||||
"blue": "Blue",
|
||||
"catégorie": "Catégorie",
|
||||
"chrono.+/-...S": "+/- ... s",
|
||||
"chrono.+10S": "+10 s",
|
||||
"chrono.+1S": "+1 s",
|
||||
"chrono.-10S": "-10 s",
|
||||
"chrono.-1S": "-1 s",
|
||||
"chrono.arrêter": "Arrêter",
|
||||
"chrono.définirLeTemps": "Définir le temps",
|
||||
"chrono.démarrer": "Démarrer",
|
||||
"chrono.editionTemps": "Edition temps",
|
||||
"chrono.entrezLeTempsEnS": "Entrez le temps en s",
|
||||
"chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}",
|
||||
"chronomètre": "Chronomètre",
|
||||
"club": "Club",
|
||||
"compétition": "Compétition",
|
||||
"compétitionManager": "Compétition manager",
|
||||
"config.obs.dossierDesResources": "Dossier des resources",
|
||||
"config.obs.motDePasseDuServeur": "Mot de passe du serveur",
|
||||
"config.obs.warn1": "/! Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en changer entre chaque compétition",
|
||||
"config.obs.ws": "ws://",
|
||||
"configurationObs": "Configuration OBS",
|
||||
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
|
||||
"confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!",
|
||||
"confirm2.title": "Changement de l'arbre du tournoi",
|
||||
"confirm3.msg": "Voulez-vous vraiment enlever la partie {{typeStr}} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !",
|
||||
"confirm3.title": "Changement de type de catégorie",
|
||||
"confirm4.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !",
|
||||
"confirm4.title": "Suppression de la catégorie",
|
||||
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
|
||||
"contre": "contre",
|
||||
"créerLesMatchs": "Créer les matchs",
|
||||
"demi-finalesEtFinales": "Demi-finales et finales",
|
||||
"duréePause": "Durée pause",
|
||||
"duréeRound": "Durée round",
|
||||
"editionDeLaCatégorie": "Edition de la catégorie",
|
||||
"enregister": "Enregister",
|
||||
"enregistrer": "Enregistrer",
|
||||
"epéeBouclier": "Epée bouclier",
|
||||
"err1": "Un combattant ne peut pas s'affronter lui-même !",
|
||||
"err2": "Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.",
|
||||
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
|
||||
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
|
||||
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
|
||||
"exporter": "Exporter",
|
||||
"fermer": "Fermer",
|
||||
"finalesUniquement": "Finales uniquement",
|
||||
"genre": "Genre",
|
||||
"genre.f": "F",
|
||||
"genre.h": "H",
|
||||
"genre.na": "NA",
|
||||
"inscrit": "Inscrit",
|
||||
"manche": "Manche",
|
||||
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
|
||||
"matches": "Matches",
|
||||
"modifier": "Modifier",
|
||||
"msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?",
|
||||
"neRienConserver": "Ne rien conserver",
|
||||
"no": "N°",
|
||||
"nom": "Nom",
|
||||
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
|
||||
"nouvelle...": "Nouvelle...",
|
||||
"obs.préfixDesSources": "Préfix des sources",
|
||||
"pays": "Pays",
|
||||
"poids": "Poids",
|
||||
"poule": "Poule",
|
||||
"poulePour": "Poule pour: ",
|
||||
"préparation...": "Préparation...",
|
||||
"rouge": "Rouge",
|
||||
"réinitialiser": "Réinitialiser",
|
||||
"résultat": "Résultat",
|
||||
"sansPoule": "Sans poule",
|
||||
"sauvegarde": "Sauvegarde",
|
||||
"sauvegarder": "Sauvegarder",
|
||||
"score": "Score",
|
||||
"score.err1": "Impossible de terminer un match nul en tournois.",
|
||||
"score.spe": "Score speciaux : <br/>-997 : disqualifié <br/>-998 : absent <br/>-999 : forfait",
|
||||
"scores": "Scores",
|
||||
"secrétariatsDeLice": "Secrétariats de lice",
|
||||
"select.aucunCombattantDisponible": "Aucun combattant disponible",
|
||||
"select.aucunCombattantSélectionné": "Aucun combattant sélectionné",
|
||||
"select.msg1": "(0 = désactivé)",
|
||||
"select.recherche": "Recherche",
|
||||
"select.sélectionnerDesCombatants": "Sélectionner des combatants",
|
||||
"select.à": "à",
|
||||
"serveur": "Serveur",
|
||||
"suivant": "Suivant",
|
||||
"supprimer": "Supprimer",
|
||||
"sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage",
|
||||
"sélectionner": "Sélectionner",
|
||||
"terminé": "Terminé",
|
||||
"texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.",
|
||||
"toast.createCategory.error": "Erreur lors de la création de la catégorie",
|
||||
"toast.createCategory.pending": "Création de la catégorie...",
|
||||
"toast.createCategory.success": "Catégorie créée !",
|
||||
"toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie",
|
||||
"toast.deleteCategory.pending": "Suppression de la catégorie...",
|
||||
"toast.deleteCategory.success": "Catégorie supprimée !",
|
||||
"toast.matchs.create.error": "Erreur lors de la création des matchs.",
|
||||
"toast.matchs.create.pending": "Création des matchs en cours...",
|
||||
"toast.matchs.create.success": "Matchs créés avec succès.",
|
||||
"toast.updateCategory.error": "Erreur lors de la mise à jour de la catégorie",
|
||||
"toast.updateCategory.pending": "Mise à jour de la catégorie...",
|
||||
"toast.updateCategory.success": "Catégorie mise à jour !",
|
||||
"toast.updateMatchScore.error": "Erreur lors de la mise à jour du score du match",
|
||||
"toast.updateMatchScore.pending": "Mise à jour du score du match...",
|
||||
"toast.updateMatchScore.success": "Score du match mis à jour !",
|
||||
"toast.updateTrees.error": "Erreur lors de la mise à jour des arbres",
|
||||
"toast.updateTrees.init.error": "Erreur lors de la création des arbres",
|
||||
"toast.updateTrees.init.pending": "Création des arbres du tournoi...",
|
||||
"toast.updateTrees.init.success": "Arbres créés !",
|
||||
"toast.updateTrees.pending": "Mise à jour des arbres du tournoi...",
|
||||
"toast.updateTrees.success": "Arbres mis à jour !",
|
||||
"tournoi": "Tournoi",
|
||||
"tournois": "Tournois",
|
||||
"tousLesMatchs": "Tous les matchs",
|
||||
"toutConserver": "Tout conserver",
|
||||
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
|
||||
"ttm.admin.scripte": "Copier le scripte d'intégration",
|
||||
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",
|
||||
"ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice",
|
||||
"ttm.table.pub_aff": "Ouvrir l'affichage public",
|
||||
"ttm.table.pub_score": "Afficher les scores sur l'affichage public",
|
||||
"type": "Type",
|
||||
"téléchargement": "Téléchargement",
|
||||
"téléchargementEnCours": "Téléchargement en cours...",
|
||||
"téléchargementTerminé!": "Téléchargement terminé !",
|
||||
"uneCatégorie": "une catégorie",
|
||||
"valider": "Valider",
|
||||
"zone": "Zone",
|
||||
"zoneDeCombat": "Zone de combat"
|
||||
}
|
||||
543
src/main/webapp/public/locales/fr/common.json
Normal file
543
src/main/webapp/public/locales/fr/common.json
Normal file
@ -0,0 +1,543 @@
|
||||
{
|
||||
"(optionnelle)": "(optionnelle)",
|
||||
"---SansClub---": "--- sans club ---",
|
||||
"---ToutLesClubs---": "--- tout les clubs ---",
|
||||
"---ToutLesPays---": "--- tout les pays ---",
|
||||
"---TouteLesCatégories---": "--- toute les catégories ---",
|
||||
"--NonLicencier--": "-- Non licencier --",
|
||||
"--SélectionnerCatégorie--": "-- Sélectionner catégorie --",
|
||||
"1Catégorie": "+1 catégorie",
|
||||
"2Catégorie": "+2 catégorie",
|
||||
"activer": "Activer",
|
||||
"admin": "Administration",
|
||||
"administrateur": "Administrateur",
|
||||
"adresse": "Adresse",
|
||||
"adresseAdministrative": "Adresse administrative",
|
||||
"aff.ancienNom": "Ancien nom: {{name}}",
|
||||
"aff.byMembreSim": "Par Membre similaire",
|
||||
"aff.byNewMenbre": "Par Nouveau membre",
|
||||
"aff.byNoLicence": "Par n° de licence",
|
||||
"aff.info1": "Ce club a déjà été affilié (affiliation n°{{no}})",
|
||||
"aff.membreNo": "Membre n°{{no}}",
|
||||
"aff.nomDuClub": "Nom du club",
|
||||
"aff.raisonDuRefus": "Raison du refus",
|
||||
"aff.raisonDuRefus.msg": "Veuillez indiquer la raison du refus",
|
||||
"aff.refusConfirm": "Êtes-vous sûr de vouloir refuser cette demande ?",
|
||||
"aff.refuserLaDemande": "Refuser la demande",
|
||||
"aff.refuserLaDemande.detail": "Êtes-vous sûr de vouloir refuser cette demande ?",
|
||||
"aff.submit.error1": "Veuillez saisir un numéro de licence valide pour le membre {{id}}",
|
||||
"aff.submit.error2": "Veuillez saisir un numéro de licence valide pour le membre {{id}}",
|
||||
"aff.submit.error3": "Veuillez saisir un email valide pour le membre {{id}}",
|
||||
"aff.toast.accept.error": "Échec de l'acceptation de l'affiliation",
|
||||
"aff.toast.accept.pending": "Acceptation de l'affiliation en cours",
|
||||
"aff.toast.accept.success": "Affiliation acceptée avec succès 🎉",
|
||||
"aff.toast.del.error": "Échec de la suppression de la demande d'affiliation",
|
||||
"aff.toast.del.pending": "Suppression de la demande d'affiliation en cours",
|
||||
"aff.toast.del.success": "Demande d'affiliation supprimée avec succès 🎉",
|
||||
"aff.toast.del2.error": "Échec de la suppression de l'affiliation",
|
||||
"aff.toast.del2.pending": "Suppression de l'affiliation en cours",
|
||||
"aff.toast.del2.success": "Affiliation supprimée avec succès 🎉",
|
||||
"aff.toast.save.error": "Échec de l'enregistrement de la demande d'affiliation",
|
||||
"aff.toast.save.pending": "Enregistrement de la demande d'affiliation en cours",
|
||||
"aff.toast.save.success": "Demande d'affiliation enregistrée avec succès 🎉",
|
||||
"aff.toast.save2.error": "Échec de l'enregistrement de l'affiliation",
|
||||
"aff.toast.save2.pending": "Enregistrement de l'affiliation en cours",
|
||||
"aff.toast.save2.success": "Affiliation enregistrée avec succès 🎉",
|
||||
"aff_req.appuyerSurRechercher": "Appuyer sur rechercher pour compléter",
|
||||
"aff_req.association": "L'association",
|
||||
"aff_req.button.cancel": "Annuler ma demande",
|
||||
"aff_req.button.confirm": "Confirmer ma demande d'affiliation",
|
||||
"aff_req.button.save": "Enregistrer les modifications",
|
||||
"aff_req.denomination": "Dénomination",
|
||||
"aff_req.disposeLicence": "Dispose déjà d'une licence",
|
||||
"aff_req.error1": "Le rôle du membre {{i}} est obligatoire",
|
||||
"aff_req.error2": "Le format du SIRET/RNA est invalide",
|
||||
"aff_req.nomDeLassociation": "Nom de l'association",
|
||||
"aff_req.text1": "L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année suivante.",
|
||||
"aff_req.text2": "Pour s’affilier, une association sportive doit réunir les conditions suivantes :",
|
||||
"aff_req.text2.li": [
|
||||
"Avoir son siège social en France ou Principauté de Monaco",
|
||||
"Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport",
|
||||
"Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération",
|
||||
"Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la Fédération",
|
||||
"Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute discrimination",
|
||||
"Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la Fédération"
|
||||
],
|
||||
"aff_req.text3": "Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF",
|
||||
"aff_req.text4": "Notez que pour finaliser votre affiliation, il vous faudra :",
|
||||
"aff_req.text4.li1": "Disposer d’au moins trois membres licenciés, dont le président",
|
||||
"aff_req.text4.li2": "S'être acquitté des cotisations prévues par les règlements fédéraux",
|
||||
"aff_req.text5": "Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux d'entrainement",
|
||||
"aff_req.text6": "Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)",
|
||||
"aff_req.text7": "Demande d'affiliation envoyée avec succès",
|
||||
"aff_req.text8": "Une fois votre demande validée, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF",
|
||||
"aff_req.toast.undo.error": "Échec de l'annulation de la demande d'affiliation",
|
||||
"aff_req.toast.undo.pending": "Annulation de la demande d'affiliation en cours",
|
||||
"aff_req.toast.undo.success": "Demande d'affiliation annulée avec succès 🎉",
|
||||
"afficherLétatDesAffiliation": "Afficher l'état des affiliation",
|
||||
"affiliation": "Affiliation",
|
||||
"affiliationNo": "Affiliation n°{{no}}",
|
||||
"ajout": "Ajout",
|
||||
"ajouterUnClub": "Ajouter un club",
|
||||
"ajouterUnMembre": "Ajouter un membre",
|
||||
"all_season": "--- tout les saisons ---",
|
||||
"au": "au",
|
||||
"aucun": "Aucun",
|
||||
"aucunMembreSélectionné": "Aucun membre sélectionné",
|
||||
"back": "« retour",
|
||||
"blason": "Blason",
|
||||
"bureau": "Bureau",
|
||||
"button.accepter": "Accepter",
|
||||
"button.ajouter": "Ajouter",
|
||||
"button.annuler": "Annuler",
|
||||
"button.appliquer": "Appliquer",
|
||||
"button.confirmer": "Confirmer",
|
||||
"button.créer": "Créer",
|
||||
"button.enregister": "Enregister",
|
||||
"button.enregistrer": "Enregistrer",
|
||||
"button.fermer": "Fermer",
|
||||
"button.modifier": "Modifier",
|
||||
"button.refuser": "Refuser",
|
||||
"button.seDésinscrire": "Se désinscrire",
|
||||
"button.suivant": "Suivant",
|
||||
"button.supprimer": "Supprimer",
|
||||
"cat.benjamin": "Benjamin",
|
||||
"cat.cadet": "Cadet",
|
||||
"cat.catégorieInconnue": "Catégorie inconnue",
|
||||
"cat.junior": "Junior",
|
||||
"cat.miniPoussin": "Mini Poussin",
|
||||
"cat.minime": "Minime",
|
||||
"cat.poussin": "Poussin",
|
||||
"cat.senior1": "Senior 1",
|
||||
"cat.senior2": "Senior 2",
|
||||
"cat.superMini": "Super Mini",
|
||||
"cat.vétéran1": "Vétéran 1",
|
||||
"cat.vétéran2": "Vétéran 2",
|
||||
"categorie": "categorie",
|
||||
"catégorie": "Catégorie",
|
||||
"certificatMédical": "Certificat médical",
|
||||
"chargement...": "Chargement...",
|
||||
"chargerLexcel": "Charger l'Excel",
|
||||
"chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.",
|
||||
"choisir...": "Choisir...",
|
||||
"club.aff_renew.msg": "Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain, vous pourrez les renseigner à la prochaine étape)",
|
||||
"club.change.status": "Pour modifier les informations ci-dessus, merci de contacter la FFSAF par mail.",
|
||||
"club.contact.tt": {
|
||||
"AUTRE": "Autre contact du club",
|
||||
"COURRIEL": "Adresse e-mail du club<br>Exemple: contact@ffsaf.fr",
|
||||
"FACEBOOK": "Page Facebook du club débutant par 'https://www.facebook.com/'</br>Exemple: https://www.facebook.com/ffmsf",
|
||||
"INSTAGRAM": "Compte Instagram du club débutant par 'https://www.instagram.com/'</br>Exemple: https://www.instagram.com/ff_msf",
|
||||
"SITE": "Site web du club avec ou sans le 'https://'</br>Exemple: ffsaf.fr</br>Ou https://ffsaf.fr",
|
||||
"TELEPHONE": "Numéro de téléphone du club<br>Exemple: 06 12 13 78 55"
|
||||
},
|
||||
"club.toast.aff.error": "Impossible de charger les affiliations",
|
||||
"club.toast.aff.pending": "Chargement des affiliations en cours",
|
||||
"club.toast.aff.success": "Affiliations chargées avec succès 🎉",
|
||||
"club.toast.del.error": "Échec de la suppression du club",
|
||||
"club.toast.del.pending": "Suppression du club en cours",
|
||||
"club.toast.del.success": "Club supprimé avec succès 🎉",
|
||||
"club.toast.new.error": "Échec de la création du club",
|
||||
"club.toast.new.pending": "Création du club en cours",
|
||||
"club.toast.new.success": "Club créé avec succès 🎉",
|
||||
"club.toast.save.error": "Échec de l'enregistrement du club",
|
||||
"club.toast.save.pending": "Enregistrement du club en cours",
|
||||
"club.toast.save.success": "Club enregistré avec succès 🎉",
|
||||
"clubExterne": "Club externe",
|
||||
"club_one": "Club",
|
||||
"club_other": "Clubs",
|
||||
"club_zero": "Sans club",
|
||||
"combattant": "combattant",
|
||||
"comp.aff.blason": "Afficher le blason du club sur les écrans",
|
||||
"comp.aff.flag": "Afficher le pays du combattant sur les écrans",
|
||||
"comp.ajoutRapide": "Ajout rapide",
|
||||
"comp.ajouterUnCombattant": "Ajouter un combattant",
|
||||
"comp.ajouterUnInvité": "Ajouter un invité",
|
||||
"comp.billetterie": "Billetterie",
|
||||
"comp.billetterieHelloasso": "Billetterie HelloAsso",
|
||||
"comp.catégorieNormalisée": "Catégorie normalisée",
|
||||
"comp.combattantNonTrouvé": "Combattant non trouvé",
|
||||
"comp.combattantsInscrits": "Combattants inscrits",
|
||||
"comp.compétitionFuture": "Compétition future",
|
||||
"comp.compétitionPassée": "Compétition passée",
|
||||
"comp.créationCompétition": "Création compétition",
|
||||
"comp.dateDinscription": "Date d'inscription",
|
||||
"comp.editionCompétition": "Edition compétition",
|
||||
"comp.error1": "La date de fin doit être postérieure à la date de début.",
|
||||
"comp.error2": "Veuillez renseigner les dates de début et de fin d'inscription.",
|
||||
"comp.error3": "La date de fin d'inscription doit être postérieure à la date de début d'inscription.",
|
||||
"comp.exporterLesInscription": "Exporter les inscription",
|
||||
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email de réception des inscriptions échoué",
|
||||
"comp.ha.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.",
|
||||
"comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.",
|
||||
"comp.ha.error3": "Veuillez renseigner l'email de réception des inscriptions échouées.",
|
||||
"comp.ha.tarifsHelloasso": "Tarifs HelloAsso",
|
||||
"comp.ha.text1": "Afin de permettre une bonne interconnexion avec HelloAsso, merci de suivre les instructions suivantes :",
|
||||
"comp.ha.text2": "<strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour qu'il redirige vers \"https://intra.ffsaf.fr/api/webhook/ha\". Pour ce faire, depuis la page d'accueil de votre association sur HelloAsso, allez dans <strong>Mon compte</strong> > <strong>Paramètres</strong> > <strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong> dans le champ <strong>Mon URL de callback</strong> et enregister.",
|
||||
"comp.ha.text3": "<strong>Copier-coller le nom exacte des tarifs</strong> <em>-sépare par des point-virgules-</em> qui donneront lieux à une inscription automatique. Tous ces tarifs doivent impérativement demander le numéro de licence en champs obligatoire. Pour ce faire, lors de la configuration de votre billetterie à l'étape n°3, cliquer sur <strong>+ Ajouter une information</strong>, saisissez l'intituler exact suivant <strong> Numéro de licence</strong>, dans <strong>Type de réponse souhaitée</strong> rentrer Nombre, sélectionner les tarifs entrés plus précédemment et rendre l'information obligatoire.",
|
||||
"comp.ha.text4": "<strong>Copier-coller l'url de votre billetterie</strong> dans le champs si dessous. Il devrais avoir la forme suivante: https://www.helloasso.com/associations/__nom-asso-sur-helloasso__/evenements/__nom-billetterie__",
|
||||
"comp.ha.text5": "Url de la billetterie HelloAsso",
|
||||
"comp.ha.text6": "Si pour une raison quelconque l'inscription automatique échoue, un email sera envoyé à cette adresse pour vous en informer",
|
||||
"comp.informationsGénéralesSurLaCompétition": "Informations générales sur la compétition",
|
||||
"comp.informationsSurLeModeDinscription": "Informations sur le mode d'inscription",
|
||||
"comp.informationsTechniques": "Informations techniques",
|
||||
"comp.inscription": "Inscription",
|
||||
"comp.inscriptionModeAdministrateur": "Inscription - mode administrateur",
|
||||
"comp.inscriptionsLibres": "Inscriptions libres",
|
||||
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Inscriptions par les administrateurs de la compétition",
|
||||
"comp.inscriptionsParLesResponsablesDeClub": "Inscriptions par les responsables de club",
|
||||
"comp.inscriptionsSurLaBilletterieHelloasso": "Inscriptions sur la billetterie HelloAsso",
|
||||
"comp.modal.information": "Information",
|
||||
"comp.modal.poids": "Poids (en kg)",
|
||||
"comp.modal.recherche": "Recherche*",
|
||||
"comp.modal.surclassement": "Surclassement",
|
||||
"comp.modal.text1": "Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via ce formulaire ne pourront pas voir leur résultat depuis leur profil.",
|
||||
"comp.modal.text2": "Empêcher les membres/club de modifier cette inscription",
|
||||
"comp.modifierLesParticipants": "Voir/Modifier les participants",
|
||||
"comp.monInscription": "Mon inscription",
|
||||
"comp.noDeLicence": "N° de licence",
|
||||
"comp.nouvelleCompétition": "Nouvelle compétition",
|
||||
"comp.organisateur": "Organisateur",
|
||||
"comp.quiPeutInscrire": "Qui peut inscrire",
|
||||
"comp.reg.libres": "Libres",
|
||||
"comp.reg.parLesAdministrateursDeLaCompétition": "Par les administrateurs de la compétition",
|
||||
"comp.reg.parLesResponsablesDeClub": "Par les responsables de club",
|
||||
"comp.reg.surLaBilletterieHelloasso": "Sur la billetterie HelloAsso",
|
||||
"comp.responsablesEtBureauxDesAssociations": "Responsables et bureaux des associations",
|
||||
"comp.sinscrire": "S'inscrire",
|
||||
"comp.supprimerLaCompétition": "Supprimer la compétition",
|
||||
"comp.supprimerLaCompétition.msg": "Êtes-vous sûr de vouloir supprimer cette compétition est tout les resultat associer?",
|
||||
"comp.surclassement_one": "{{cat}} avec 1 de surclassement",
|
||||
"comp.surclassement_other": "{{cat}} avec {{count}} de surclassements",
|
||||
"comp.surclassement_zero": "{{cat}}",
|
||||
"comp.text2": "Visible par le public (Apparaît dans la liste des compétitions)",
|
||||
"comp.text3": "Si non coché, la compétition ne sera visible que par les personnes pouvant y inscrire des participants.",
|
||||
"comp.tips": "Tips 1: Il est possible de bannir un combattant, ce qui l'empêchera d'être réinscrit par un autre moyen que par un administrateur de cette compétition. Pour cela, cliquez sur la petite <1/> à côté de son nom.<br/>Tips 2: Il est aussi possible de verrouiller les modifications de son inscription depuis sa fiche, ce qui l'empêchera d'être modifié/supprimé par lui-même et/ou un responsable de club.",
|
||||
"comp.toast.del.error": "Échec de la suppression de la compétition",
|
||||
"comp.toast.del.pending": "Suppression de la compétition en cours",
|
||||
"comp.toast.del.success": "Compétition supprimée avec succès 🎉",
|
||||
"comp.toast.params.error": "Échec de la mise à jours des paramètres de la compétition",
|
||||
"comp.toast.params.pending": "Mise à jours des paramètres de la compétition en cours...",
|
||||
"comp.toast.params.success": "Paramètres de la compétition mis à jours avec succès 🎉",
|
||||
"comp.toast.register.add.error": "Combattant non trouvé",
|
||||
"comp.toast.register.add.pending": "Recherche en cours",
|
||||
"comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour",
|
||||
"comp.toast.register.ban.error": "Erreur",
|
||||
"comp.toast.register.ban.pending": "Désinscription en cours",
|
||||
"comp.toast.register.ban.success": "Combattant désinscrit et bannie",
|
||||
"comp.toast.register.del.error": "Erreur",
|
||||
"comp.toast.register.del.pending": "Désinscription en cours",
|
||||
"comp.toast.register.del.success": "Combattant désinscrit",
|
||||
"comp.toast.register.self.add.error": "Erreur lors de l'inscription",
|
||||
"comp.toast.register.self.add.pending": "Inscription en cours",
|
||||
"comp.toast.register.self.add.success": "Inscription réalisée 🎉",
|
||||
"comp.toast.register.self.del.error": "Erreur lors de la désinscription",
|
||||
"comp.toast.register.self.del.pending": "Désinscription en cours",
|
||||
"comp.toast.register.self.del.success": "Désinscription réalisée",
|
||||
"comp.toast.save.error": "Échec de l'enregistrement de la compétition",
|
||||
"comp.toast.save.pending": "Enregistrement de la compétition en cours",
|
||||
"comp.toast.save.success": "Compétition enregistrée avec succès 🎉",
|
||||
"comp.tousLesMembresDeLaFfsaf": "Tous les membres de la FFSAF",
|
||||
"comp.typeDinscription": "Type d'inscription",
|
||||
"comp.uniquementLesAdministrateursDeLaCompétition": "Uniquement les administrateurs de la compétition",
|
||||
"comp.warn1": "Êtes-vous sûr de vouloir désinscrire et bannir ce combattant de la compétition?\n(Vous pouvez le réinscrire plus tard)\"",
|
||||
"comp.warn2": "Êtes-vous sûr de vouloir désinscrire ce combattant ?\nCela ne le désinscrira pas de la billetterie HelloAsso et ne le remboursera pas.",
|
||||
"comp.warn3": "Êtes-vous sûr de vouloir désinscrire ce combattant ?",
|
||||
"comp.warn4": "Êtes-vous sûr de vouloir vous désinscrire ?",
|
||||
"comp_manage": "Compétitions Manager",
|
||||
"competition_one": "Compétition",
|
||||
"competition_other": "Compétitions",
|
||||
"compte": "Compte",
|
||||
"compétition": "Compétition",
|
||||
"configuration": "Configuration",
|
||||
"conserverLancienEmail": "Conserver l'ancien email",
|
||||
"contactAdministratif": "Contact administratif",
|
||||
"contactInterne": "Contact interne",
|
||||
"contact_one": "Contact",
|
||||
"contact_other": "Contacts",
|
||||
"date": "Date",
|
||||
"dateDeNaissance": "Date de naissance",
|
||||
"days": [
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi",
|
||||
"Dimanche"
|
||||
],
|
||||
"de": "de",
|
||||
"demandeDaffiliationEnCours": "Demande d'affiliation en cours",
|
||||
"demandeDeLicence": "Demande de licence ",
|
||||
"demander": "Demander",
|
||||
"description": "Description",
|
||||
"dlAff": "Téléchargée l'attestation d'affiliation",
|
||||
"donnéesAdministratives": "Données administratives",
|
||||
"du": "Du",
|
||||
"dun": "d'un",
|
||||
"définirLidDuCompte": "Définir l'id du compte",
|
||||
"editionDeL'affiliation": "Edition de l'affiliation",
|
||||
"editionDeLaDemande": "Edition de la demande ",
|
||||
"editionDeLaLicence": "Edition de la licence",
|
||||
"editionDeLaSéléction": "Edition de la séléction",
|
||||
"editionDeLadresse": "Edition de l'adresse",
|
||||
"email": "Email",
|
||||
"en": "en",
|
||||
"enAttente": "En attente",
|
||||
"erreurDePaiement": "Erreur de paiement😕",
|
||||
"erreurDePaiement.detail": "Message d'erreur :",
|
||||
"erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.",
|
||||
"espaceAdministration": "Espace administration",
|
||||
"f": "F",
|
||||
"faitPar": "Fait par",
|
||||
"femme": "Femme",
|
||||
"filtre": "Filtre",
|
||||
"genre": "Genre",
|
||||
"gestionGroupée": "Gestion groupée",
|
||||
"gradeDarbitrage": "Grade d'arbitrage",
|
||||
"h": "H",
|
||||
"home": {
|
||||
"header1": "Pour les licenciés",
|
||||
"header2": "Pour les clubs",
|
||||
"text1": "Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que le club organisateur les ait renseignés.<br/><br/>Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé une fois votre licence validée par le secrétariat.",
|
||||
"text2": "C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur le site ffsaf.fr.<br/>Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi que d'enregistrer les résultats.<br/><br/> Vous n'êtes pas encore affilié à la fédération ? Cliquez <1>içi</1> pour faire votre première demande.",
|
||||
"welcome_message": "Bienvenue sur l’intranet de la Fédération France Soft Armored Fighting"
|
||||
},
|
||||
"homme": "Homme",
|
||||
"horairesD'entraînements": "Horaires d'entraînements",
|
||||
"information": "Information",
|
||||
"invité": "invité",
|
||||
"keepEmpty": "Laissez vide pour ne rien changer.",
|
||||
"le": "le",
|
||||
"licence": "Licence",
|
||||
"licenceNo": "Licence n°{{no}}",
|
||||
"lieu": "Lieu",
|
||||
"lieuxDentraînements": "Lieux d'entraînements",
|
||||
"loading": "Chargement...",
|
||||
"me": {
|
||||
"result": {
|
||||
"PRIVATE": "Privé (visible uniquement par moi)",
|
||||
"PUBLIC": "Public (visible par tous)",
|
||||
"REGISTERED_ONLY": "Membres connectés (visibles par les membres de la fédération)",
|
||||
"REGISTERED_ONLY_NO_DETAILS": "Membres connectés - masquer les détails (visibles par les membres de la fédération)"
|
||||
},
|
||||
"toast.settings.error": "Échec de la mise à jours des paramètres 😕",
|
||||
"toast.settings.pending": "Mise à jours des paramètres en cours...",
|
||||
"toast.settings.success": "Paramètres mis à jours avec succès 🎉"
|
||||
},
|
||||
"me.changerMonMotDePasse": "Changer mon mot de passe",
|
||||
"me.formationDarbitrage": "Formation d'arbitrage",
|
||||
"me.paramètresDuCompte": "Paramètres du compte",
|
||||
"me.rôleAuSienDuClub": "Rôle au sien du club",
|
||||
"me.visibilitéDesRésultats": "Visibilité des résultats",
|
||||
"member_one": "Membre",
|
||||
"member_other": "Membres",
|
||||
"membre.emailVideàLaLigne": "Email vide à la ligne {{no}}",
|
||||
"membre.emailVérifié": "Email vérifié",
|
||||
"membre.filtre.inactif": "Afficher les combattants inactifs",
|
||||
"membre.filtre.licence": "Afficher l'état des licences",
|
||||
"membre.filtre.licences": [
|
||||
"Sans demande ni licence validée",
|
||||
"Avec demande ou licence validée",
|
||||
"Demande en cours",
|
||||
"Licence validée",
|
||||
"Tout les états de licences",
|
||||
"Demande complet",
|
||||
"Demande incomplet"
|
||||
],
|
||||
"membre.filtre.payement": [
|
||||
"Sans paiement",
|
||||
"Avec paiement",
|
||||
"Tout les états de paiement"
|
||||
],
|
||||
"membre.identifiant": "Identifiant",
|
||||
"membre.import.err1": "Format de la date de certificat invalide à la ligne {{no}}",
|
||||
"membre.import.err2": "Format de la date de certificat invalide à la ligne {{no}}",
|
||||
"membre.import.err3": "Date de naissance vide à la ligne {{no}}",
|
||||
"membre.import.err4": "Format de la date de naissance invalide à la ligne {{no}}",
|
||||
"membre.import.err5": "Email invalide à la ligne {{no}}",
|
||||
"membre.import.errTT_one": "{{count}} erreur dans le fichier, opération annulée",
|
||||
"membre.import.errTT_other": "{{count}} erreurs dans le fichier, opération annulée",
|
||||
"membre.import.warn_one": "{{count}} certificat médical non rempli",
|
||||
"membre.import.warn_other": "{{count}} certificats médicaux non remplis",
|
||||
"membre.info.emailInfo": "L'email sert à la création de compte pour se connecter au site et doit être unique.<br/>Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+<caractères alphanumériques>@exemple.com'}.<br/>Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com",
|
||||
"membre.info.error1": "Veuillez sélectionner un pays valide 😕",
|
||||
"membre.info.error2": "Veuillez sélectionner un club valide 😕",
|
||||
"membre.initaccount": "Initialiser le compte",
|
||||
"membre.initaccount.text1": "Entré l'UUID du compte",
|
||||
"membre.initaccount.text2": "Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...",
|
||||
"membre.noAccount": "Ce membre ne dispose pas de compte...",
|
||||
"membre.noAccount.clubMsg": "Un compte sera créé par la fédération lors de la validation de sa première licence",
|
||||
"membre.nomVideàLaLigne": "Nom vide à la ligne {{no}}",
|
||||
"membre.prénomVideàLaLigne": "Prénom vide à la ligne {{no}}",
|
||||
"membre.toast.compte.created": "Compte créé avec succès 🎉",
|
||||
"membre.toast.compte.error": "Échec de la création du compte",
|
||||
"membre.toast.compte.pending": "Création du compte en cours",
|
||||
"membre.toast.del.error": "Échec de la suppression du compte",
|
||||
"membre.toast.del.pending": "Suppression du compte en cours",
|
||||
"membre.toast.del.success": "Compte supprimé avec succès 🎉",
|
||||
"membre.toast.id.error": "Échec de la définition de l'identifient",
|
||||
"membre.toast.id.pending": "Définition de l'identifient en cours",
|
||||
"membre.toast.id.success": "Identifient défini avec succès 🎉",
|
||||
"membre.toast.licence.ask.del.error": "Échec de la suppression de la demande de licence",
|
||||
"membre.toast.licence.ask.del.pending": "Suppression de la demande de licence en cours",
|
||||
"membre.toast.licence.ask.del.success": "Demande de licence supprimée avec succès 🎉",
|
||||
"membre.toast.licence.ask.error": "Échec de la demande de licence",
|
||||
"membre.toast.licence.ask.pending": "Enregistrement de la demande de licence en cours",
|
||||
"membre.toast.licence.ask.success": "Demande de licence enregistrée avec succès 🎉",
|
||||
"membre.toast.licence.del.error": "Échec de la suppression de la licence",
|
||||
"membre.toast.licence.del.pending": "Suppression de la licence en cours",
|
||||
"membre.toast.licence.del.success": "Licence supprimée avec succès 🎉",
|
||||
"membre.toast.licence.save.error": "Échec de l'enregistrement de la licence",
|
||||
"membre.toast.licence.save.pending": "Enregistrement de la licence en cours",
|
||||
"membre.toast.licence.save.success": "Licence enregistrée avec succès 🎉",
|
||||
"membre.toast.licences.export.error": "Échec de l'export des licences",
|
||||
"membre.toast.licences.export.pending": "Export des licences en cours",
|
||||
"membre.toast.licences.export.success": "Licences exportées avec succès 🎉",
|
||||
"membre.toast.licences.import.error": "Échec de l'envoie des changements",
|
||||
"membre.toast.licences.import.pending": "Envoie des changements en cours",
|
||||
"membre.toast.licences.import.success": "Changements envoyés avec succès 🎉",
|
||||
"membre.toast.licences.load.error": "Impossible de charger les licences",
|
||||
"membre.toast.licences.load.pending": "Chargement des licences en cours",
|
||||
"membre.toast.licences.load.success": "Licences chargées avec succès 🎉",
|
||||
"membre.toast.perm.error": "Échec de la mise à jours des permissions 😕",
|
||||
"membre.toast.perm.pending": "Mise à jours des permissions en cours...",
|
||||
"membre.toast.perm.success": "Permission mise à jours avec succès 🎉",
|
||||
"membre.toast.save.error": "Échec de la mise à jours du profil 😕",
|
||||
"membre.toast.save.pending": "Mise à jours du profil en cours...",
|
||||
"membre.toast.save.success": "Profil mis à jours avec succès 🎉",
|
||||
"membre.toast.select.del.error": "Échec de la suppression de la séléction",
|
||||
"membre.toast.select.del.pending": "Suppression de la séléction en cours",
|
||||
"membre.toast.select.del.success": "Séléction supprimée avec succès 🎉",
|
||||
"membre.toast.select.save.error": "Échec de l'enregistrement de la séléction",
|
||||
"membre.toast.select.save.pending": "Enregistrement de la séléction en cours",
|
||||
"membre.toast.select.save.success": "Séléction enregistrée avec succès 🎉",
|
||||
"mettreàJours": "Mettre à jours",
|
||||
"modification": "Modification",
|
||||
"nationalité": "Nationalité",
|
||||
"nav": {
|
||||
"account": "Mon compte",
|
||||
"aff_request": "Demande d'affiliation",
|
||||
"club": {
|
||||
"my": "Mon club"
|
||||
},
|
||||
"competitions": {
|
||||
"results": "Mes résultats"
|
||||
},
|
||||
"home": "Accueil",
|
||||
"login": "Connexion",
|
||||
"logout": "Déconnexion",
|
||||
"space": "Mon espace",
|
||||
"title": "FFSAF Intranet"
|
||||
},
|
||||
"noLicence": "N° Licence",
|
||||
"noSiretOuRna": "N° $t(siretOuRna)",
|
||||
"nom": "Nom",
|
||||
"nombreDeLicences": "Nombre de licences",
|
||||
"nombreDeLicencesParCatégorie": "Nombre de licences par catégorie pour {{saison}}",
|
||||
"non": "Non",
|
||||
"nonDéfinie": "Non définie",
|
||||
"nonValidée": "Non validée",
|
||||
"nouveauClub": "Nouveau club",
|
||||
"nouveauMembre": "Nouveau membre",
|
||||
"nouvelEmail": "Nouvel email",
|
||||
"ou": "Ou",
|
||||
"oui": "Oui",
|
||||
"outdated_session": {
|
||||
"login_button": "Se reconnecter",
|
||||
"message": "Votre session a expirée, veuillez vous reconnecter pour continuer à utiliser l'application.",
|
||||
"title": "Session expirée"
|
||||
},
|
||||
"pageClub": "Page club",
|
||||
"pageMembre": "Page membre",
|
||||
"pageNouveauClub": "Page nouveau club",
|
||||
"page_info_full": "Ligne {{line}} à {{tt_line}} (page {{page}} sur {{tt_page}})",
|
||||
"page_info_ligne": "{{show}} ligne(s) affichée(s) sur {{total}}",
|
||||
"paiementDeLaLicence": "Paiement de la licence",
|
||||
"paiementDesLicences": "Paiement des licences",
|
||||
"par": "par",
|
||||
"pasDeLicence": "Pas de licence",
|
||||
"payment.ha.info": "Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à l’association choisie. Vous pouvez soutenir l’aide qu’ils apportent aux associations en laissant une contribution volontaire à HelloAsso au moment de votre paiement.",
|
||||
"payment.info": "A propos de HelloAsso",
|
||||
"payment.paiementSécurisé": "Paiement sécurisé",
|
||||
"payment.payerAvec": "Payer avec",
|
||||
"payment.recap": "{{count}} licence(s) sélectionnée<br/>Total à régler : {{total}} €",
|
||||
"paymentDesLicences": "Payment des licences",
|
||||
"paymentDesLicences.msg_one": "Êtes-vous sûr de vouloir marquer comme payées la licence ?",
|
||||
"paymentDesLicences.msg_other": "Êtes-vous sûr de vouloir marquer comme payées les {{count}} licences ?",
|
||||
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
|
||||
"paymentOk": "🎉Votre paiement a été traité avec succès.🎉",
|
||||
"paymentOk.msg": "Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.",
|
||||
"pays": "Pays",
|
||||
"perm.administrateurDeLaFédération": "Administrateur de la fédération",
|
||||
"perm.créerDesCompétion": "Créer des compétion",
|
||||
"perm.ffsafIntra": "FFSAF intra",
|
||||
"permission": "Permission",
|
||||
"photos": "Photos",
|
||||
"prenom": "Prénom",
|
||||
"prénomEtNom": "Prénom et nom",
|
||||
"rechercher": "Rechercher",
|
||||
"rechercher...": "Rechercher...",
|
||||
"registration_one": "Inscription",
|
||||
"registration_other": "Inscriptions",
|
||||
"renouveler": "Renouveler",
|
||||
"renouvellementDeLaffiliation": "Renouvellement de l'affiliation",
|
||||
"result_one": "Résultat",
|
||||
"result_other": "Résultats",
|
||||
"retouràLaListeDeMembres": "Retour à la liste de membres",
|
||||
"role": "Rôle",
|
||||
"role.membre": "Membre",
|
||||
"role.membreDuBureau": "Membre du bureau",
|
||||
"role.président": "Président",
|
||||
"role.secrétaire": "Secrétaire",
|
||||
"role.trésorier": "Trésorier",
|
||||
"role.vise-président": "Vise-Président",
|
||||
"role.vise-secrétaire": "Vise-Secrétaire",
|
||||
"role.vise-trésorier": "Vise-Trésorier",
|
||||
"saison": "Saison",
|
||||
"secrétariatsDeLice": "Secrétariats de lice",
|
||||
"selectionner...": "Sélectionner...",
|
||||
"siretOuRna": "SIRET ou RNA",
|
||||
"stats": "Statistiques",
|
||||
"statue": "Statue",
|
||||
"status": "Status",
|
||||
"statuts": "Statuts",
|
||||
"supprimerLeClub": "Supprimer le club",
|
||||
"supprimerLeClub.msg": "Êtes-vous sûr de vouloir supprimer ce club ?",
|
||||
"supprimerLeCompte": "Supprimer le compte",
|
||||
"supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?",
|
||||
"sélectionEnéquipeDeFrance": "Sélection en équipe de France",
|
||||
"sélectionner...": "Sélectionner...",
|
||||
"toast.edit.error": "Échec de l'enregistrement des modifications",
|
||||
"toast.edit.pending": "Enregistrement des modifications en cours",
|
||||
"toast.edit.success": "Modifications enregistrées avec succès 🎉",
|
||||
"toast.licence.bulk.pay.error": "Échec du marquage des licences comme payées",
|
||||
"toast.licence.bulk.pay.pending": "Marquage des licences comme payées en cours",
|
||||
"toast.licence.bulk.pay.success": "Licences marquées comme payées avec succès 🎉",
|
||||
"toast.licence.bulk.valid.error": "Échec de la validation des licences",
|
||||
"toast.licence.bulk.valid.pending": "Validation des licences en cours",
|
||||
"toast.licence.bulk.valid.success": "Licences validées avec succès 🎉",
|
||||
"toast.licence.order.error": "Échec de le création de la commande",
|
||||
"toast.licence.order.pending": "Création de la commande en cours",
|
||||
"toast.licence.order.success": "Commande créée avec succès 🎉",
|
||||
"trie": "Trie",
|
||||
"téléchargerLexcelDesMembres": "Télécharger l'Excel des membres",
|
||||
"téléchargerLexcelDesMembres.info": "À utiliser comme template pour mettre à jour les informations",
|
||||
"téléchargéeLaLicence": "Téléchargée la licence",
|
||||
"validationDeLaLicence": "Validation de la licence",
|
||||
"validationDesLicences": "Validation des licences",
|
||||
"validerDesLicences": "Valider des licences",
|
||||
"validerLePayement_one": "Valider le payement de la licence sélectionnée",
|
||||
"validerLePayement_other": "Valider le payement des {{count}} licences sélectionnées",
|
||||
"validerLePayement_zero": "$t(validerLePayement_other)",
|
||||
"validerLicence.msg_one": "Êtes-vous sûr de vouloir valider la licence ?",
|
||||
"validerLicence.msg_other": "Êtes-vous sûr de vouloir valider les {{count}} licences ?",
|
||||
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
|
||||
"validerLicence_one": "Valider la licence sélectionnée",
|
||||
"validerLicence_other": "Valider les {{count}} licences sélectionnées",
|
||||
"validerLicence_zero": "$t(validerLicence_other)",
|
||||
"validée": "Validée",
|
||||
"voir/modifierLesParticipants": "Voir/Modifier les participants",
|
||||
"voirLesStatues": "Voir les statues",
|
||||
"à": "à",
|
||||
"étatDeLaDemande": "État de la demande"
|
||||
}
|
||||
67
src/main/webapp/public/locales/fr/result.json
Normal file
67
src/main/webapp/public/locales/fr/result.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"--sélectionnerUnClub--": "--Sélectionner un club--",
|
||||
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
|
||||
"--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--",
|
||||
"abs.": "abs.",
|
||||
"adversaire": "Adversaire",
|
||||
"aujourdhuià": "Aujourd'hui à {{time}}",
|
||||
"avanthierà": "Avant-hier à {{time}}",
|
||||
"back": "« retour",
|
||||
"bleu": "Bleu",
|
||||
"catégorie": "Catégorie",
|
||||
"chargement": "Chargement",
|
||||
"club": "Club",
|
||||
"combattant": "Combattant",
|
||||
"combattants": "Combattants",
|
||||
"date": "Date",
|
||||
"disc.": "disc.",
|
||||
"défaites": "Défaites",
|
||||
"erreurDeChargementDeLaListe": "Erreur de chargement de la liste",
|
||||
"erreurDeChargementDeLaPoule": "Erreur de chargement de la poule",
|
||||
"erreurDeChargementDesCatégories": "Erreur de chargement des catégories",
|
||||
"erreurDeChargementDesClubs": "Erreur de chargement des clubs",
|
||||
"erreurDeChargementDesCombattants": "Erreur de chargement des combattants",
|
||||
"erreurDeChargementDuClub": "Erreur de chargement du club",
|
||||
"erreurDeChargementDuCombattant": "Erreur de chargement du combattant",
|
||||
"for.": "for.",
|
||||
"hierà": "Hier à {{time}}",
|
||||
"info": "Info",
|
||||
"listeDesCombattants": "Liste des combattants",
|
||||
"listeDesMatchs": "Liste des matchs",
|
||||
"listeDesMembres": "Liste des membres",
|
||||
"listeDesMenbres": "Liste des menbres",
|
||||
"nom": "Nom",
|
||||
"nomPrénom": "Nom Prénom",
|
||||
"nombreDeMatchDisputé2": "Nombre de match disputé : {{nb}}",
|
||||
"nombreDeVictoires2": "Nombre de victoires : {{nb}} ",
|
||||
"nombreDinscris": "Nombre d'inscris",
|
||||
"nombreDinscris2": "Nombre d'inscris : {{nb}}",
|
||||
"parCatégorie": "Par catégorie",
|
||||
"parClub": "Par club",
|
||||
"parCombattant": "Par combattant",
|
||||
"place": "Place",
|
||||
"pointsMarqués": "Points marqués",
|
||||
"pointsMarqués2": "Points marqués : {{nb}}",
|
||||
"pointsReçus": "Points reçus",
|
||||
"pointsReçus2": "Points reçus : {{nb}}",
|
||||
"poule": "Poule",
|
||||
"ratio": "Ratio",
|
||||
"ratioDePointsMoyen2": "Ratio de points moyen : {{nb}}",
|
||||
"ratioDeVictoiresMoyen2": "Ratio de victoires moyen : {{nb}}",
|
||||
"ratioDuScore2": "Ratio du score (point marqué / point reçu): {{nb}}",
|
||||
"ratioPoints": "Ratio points",
|
||||
"ratioVictoires": "Ratio victoires",
|
||||
"ratiosPoints": "Ratios points",
|
||||
"rechercheParCatégorie": "Recherche par catégorie",
|
||||
"rechercheParClub": "Recherche par club",
|
||||
"rechercheParCombattant": "Recherche par combattant",
|
||||
"rouge": "Rouge",
|
||||
"résultatDeLaCompétition": "Résultat de la compétition",
|
||||
"scores": "Scores",
|
||||
"statistique": "Statistique",
|
||||
"tauxDeVictoire2": "Taux de victoire : {{nb}}% ({{victoires}} sur {{matchs}})",
|
||||
"tournois": "Tournois",
|
||||
"tousLesCombattants": "Tous les combattants",
|
||||
"victoire": "Victoire",
|
||||
"victoires": "Victoires"
|
||||
}
|
||||
@ -14,8 +14,8 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
|
||||
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
|
||||
import {MePage} from "./pages/MePage.jsx";
|
||||
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
|
||||
import {FallingLines} from "react-loader-spinner";
|
||||
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -79,10 +79,12 @@ const router = createBrowserRouter([
|
||||
|
||||
function GetCompetitionMangerLazy() {
|
||||
const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx'))
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <Suspense
|
||||
fallback={<div>
|
||||
<h1>Compétition manager</h1>
|
||||
<p>Chargement...</p>
|
||||
<h1>{t("comp_manage")}</h1>
|
||||
<p>{t("loading")}</p>
|
||||
</div>}>
|
||||
<CMLazy/>
|
||||
</Suspense>
|
||||
@ -144,6 +146,7 @@ function Root() {
|
||||
function ReAuthMsg() {
|
||||
const {is_authenticated} = useAuth()
|
||||
const location = useLocation()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const notAuthPaths = [
|
||||
/^\/$/s,
|
||||
@ -161,15 +164,14 @@ function ReAuthMsg() {
|
||||
}}>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5>Session expirée</h5>
|
||||
<h5>{t("outdated_session.title")}</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="card-text">Votre session a expirée, veuillez vous reconnecter pour continuer à
|
||||
utiliser l'application.</p>
|
||||
<p className="card-text">{t("outdated_session.message")}</p>
|
||||
</div>
|
||||
<div className="card-footer">
|
||||
<button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>Se reconnecter</button>
|
||||
<a className="btn btn-secondary" href="/">Accueil</a>
|
||||
<button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>{t("outdated_session.login_button")}</button>
|
||||
<a className="btn btn-secondary" href="/">{t("nav.home")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,19 +2,12 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faAdd, faCircleQuestion, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ContactEditor({data}) {
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
const [out_data, setOutData] = useState({})
|
||||
|
||||
const tooltipText = {
|
||||
SITE: "Site web du club avec ou sans le 'https://'</br>Exemple: ffsaf.fr</br>Ou https://ffsaf.fr",
|
||||
FACEBOOK: "Page Facebook du club débutant par 'https://www.facebook.com/'</br>Exemple: https://www.facebook.com/ffmsf",
|
||||
TELEPHONE: "Numéro de téléphone du club<br>Exemple: 06 12 13 78 55",
|
||||
INSTAGRAM: "Compte Instagram du club débutant par 'https://www.instagram.com/'</br>Exemple: https://www.instagram.com/ff_msf",
|
||||
COURRIEL: "Adresse e-mail du club<br>Exemple: contact@ffsaf.fr",
|
||||
AUTRE: "Autre contact du club",
|
||||
}
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
let i = 0;
|
||||
@ -38,7 +31,7 @@ export function ContactEditor({data}) {
|
||||
|
||||
return <div className="row mb-3">
|
||||
<input name="contact" value={JSON.stringify(out_data)} readOnly hidden/>
|
||||
<span className="input-group-text">Contacts</span>
|
||||
<span className="input-group-text">{t('contact', {count : 2})}</span>
|
||||
<ul className="list-group form-control">
|
||||
{state.map((d, index) => {
|
||||
if (d.data === undefined || d.data.value === undefined)
|
||||
@ -62,7 +55,7 @@ export function ContactEditor({data}) {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: {type: d.data.type, value: e.target.value}}})
|
||||
}}/>
|
||||
<button type="button" className="btn btn-outline-info" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
data-bs-title={tooltipText[d.data.type]} data-bs-html="true">
|
||||
data-bs-title={t("club.contact.tt." + d.data.type)} data-bs-html="true">
|
||||
<FontAwesomeIcon icon={faCircleQuestion}/>
|
||||
</button>
|
||||
<button className="btn btn-danger" type="button"
|
||||
|
||||
@ -2,6 +2,7 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function timeNumberToSting(nbMin) {
|
||||
return String(Math.floor(nbMin / 60)).padStart(2, '0') + ":" + String(nbMin % 60).padStart(2, '0')
|
||||
@ -15,6 +16,7 @@ function timeStringToNumber(time) {
|
||||
export function HoraireEditor({data}) {
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
const [out_data, setOutData] = useState({})
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data.training_day_time === null)
|
||||
@ -38,7 +40,7 @@ export function HoraireEditor({data}) {
|
||||
|
||||
return <div className="row mb-3">
|
||||
<input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/>
|
||||
<span className="input-group-text">Horaires d'entraînements</span>
|
||||
<span className="input-group-text">{t("horairesD'entraînements")}</span>
|
||||
<ul className="list-group form-control">
|
||||
{state.map((d, index) => {
|
||||
return <div key={index} className="input-group">
|
||||
@ -48,22 +50,22 @@ export function HoraireEditor({data}) {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
|
||||
dispatch({type: 'SORT', payload: sortHoraire})
|
||||
}}>
|
||||
<option value="0">Lundi</option>
|
||||
<option value="1">Mardi</option>
|
||||
<option value="2">Mercredi</option>
|
||||
<option value="3">Jeudi</option>
|
||||
<option value="4">Vendredi</option>
|
||||
<option value="5">Samedi</option>
|
||||
<option value="6">Dimanche</option>
|
||||
<option value="0">{t('days.0')}</option>
|
||||
<option value="1">{t('days.1')}</option>
|
||||
<option value="2">{t('days.2')}</option>
|
||||
<option value="3">{t('days.3')}</option>
|
||||
<option value="4">{t('days.4')}</option>
|
||||
<option value="5">{t('days.5')}</option>
|
||||
<option value="6">{t('days.6')}</option>
|
||||
</select>
|
||||
<span className="input-group-text">de</span>
|
||||
<span className="input-group-text">{t('de')}</span>
|
||||
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_start)} required
|
||||
onChange={(e) => {
|
||||
d.data.time_start = timeStringToNumber(e.target.value)
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
|
||||
dispatch({type: 'SORT', payload: sortHoraire})
|
||||
}}/>
|
||||
<span className="input-group-text">à</span>
|
||||
<span className="input-group-text">{t('à')}</span>
|
||||
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_end)} required
|
||||
onChange={(e) => {
|
||||
d.data.time_end = timeStringToNumber(e.target.value)
|
||||
|
||||
@ -5,10 +5,12 @@ import proj4 from "proj4";
|
||||
import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {MapContainer, Marker, TileLayer} from "react-leaflet";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function LocationEditor({data, setModal, sendData}) {
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
const [out_data, setOutData] = useState({})
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data.training_location === null)
|
||||
@ -42,7 +44,7 @@ export function LocationEditor({data, setModal, sendData}) {
|
||||
|
||||
return <div className="row mb-3">
|
||||
<input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/>
|
||||
<span className="input-group-text">Lieux d'entraînements</span>
|
||||
<span className="input-group-text">{t('lieuxDentraînements')}</span>
|
||||
<ul className="list-group form-control">
|
||||
{state.map((d, index) => {
|
||||
return <div key={index} className="input-group">
|
||||
@ -83,8 +85,9 @@ export function LocationEditorModal({modal, sendData}) {
|
||||
const [location, setLocation] = useState("")
|
||||
const [locationObj, setLocationObj] = useState({text: "", lng: undefined, lat: undefined})
|
||||
const [mapPosition, setMapPosition] = useState([46.652195, 2.430226])
|
||||
const {data, error, refresh} = useFetch(null)
|
||||
const {data, refresh} = useFetch(null)
|
||||
const map = useRef(null)
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (modal.data !== undefined) {
|
||||
@ -136,7 +139,7 @@ export function LocationEditorModal({modal, sendData}) {
|
||||
<form onSubmit={e => sendData.current(e)}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="EditModalLabel">Edition de l'adresse</h1>
|
||||
<h1 className="modal-title fs-5" id="EditModalLabel">{t('editionDeLadresse')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
@ -147,7 +150,7 @@ export function LocationEditorModal({modal, sendData}) {
|
||||
<input name="loc_lng" value={locationObj.lng ? locationObj.lng : -142} readOnly hidden/>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text">Adresse</label>
|
||||
<label className="input-group-text">{t('adresse')}</label>
|
||||
<input className="form-control" aria-autocomplete="list" aria-expanded="true"
|
||||
placeholder="Chercher une adresse..." aria-label="Recherche" list="addr" value={location}
|
||||
onChange={e => setLocation(e.target.value)}/>
|
||||
@ -174,9 +177,9 @@ export function LocationEditorModal({modal, sendData}) {
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal"
|
||||
disabled={locationObj.lng === undefined}>Enregistrer
|
||||
disabled={locationObj.lng === undefined}>{t('button.enregistrer')}
|
||||
</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../hooks/useFetch.js";
|
||||
import {AxiosError} from "./AxiosError.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ClubSelect({defaultValue, name, na = false, disabled = false}) {
|
||||
return <LoadingProvider>
|
||||
@ -11,6 +12,7 @@ export function ClubSelect({defaultValue, name, na = false, disabled = false}) {
|
||||
function ClubSelect_({defaultValue, name, na, disabled}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{data
|
||||
@ -18,8 +20,8 @@ function ClubSelect_({defaultValue, name, na, disabled}) {
|
||||
<label className="input-group-text" id="inputGroupSelect02">Club</label>
|
||||
<select className="form-select" id="inputGroupSelect02" disabled={disabled}
|
||||
defaultValue={defaultValue ? defaultValue : -1} name={name}>
|
||||
<option>Sélectionner...</option>
|
||||
{na && <option value={-1}>-- Non licencier --</option>}
|
||||
<option>{t('sélectionner...')}</option>
|
||||
{na && <option value={-1}>{t('--NonLicencier--')}</option>}
|
||||
{data.map(club => (<option key={club.id} value={club.id}>{club.name}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
@ -31,11 +33,13 @@ function ClubSelect_({defaultValue, name, na, disabled}) {
|
||||
}
|
||||
|
||||
function Def() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="input-group mb-3">
|
||||
<label className="input-group-text" id="inputGroupSelect02">Club</label>
|
||||
<label className="input-group-text" id="inputGroupSelect02">{t("club", {count: 1})}</label>
|
||||
<select className="form-select" id="inputGroupSelect02"
|
||||
defaultValue="Chargement...">
|
||||
<option>Chargement...</option>
|
||||
defaultValue={t('chargement...')}>
|
||||
<option>{t('chargement...')}</option>
|
||||
</select>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {Fragment} from "react";
|
||||
import './ColoredCircle.css'
|
||||
import i18n from "i18next";
|
||||
|
||||
export const ColoredCircle = ({color, boolean}) => {
|
||||
const styles = {backgroundColor: '#F00'};
|
||||
@ -15,7 +16,7 @@ export const ColoredCircle = ({color, boolean}) => {
|
||||
</Fragment>
|
||||
};
|
||||
|
||||
export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => {
|
||||
export const ColoredText = ({boolean, text={true: i18n.t('oui'), false: i18n.t('non')}}) => {
|
||||
const styles = {color: '#F00'};
|
||||
|
||||
if (boolean !== undefined) {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) {
|
||||
const {t} = useTranslation();
|
||||
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">
|
||||
@ -10,8 +12,8 @@ export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel =
|
||||
{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>
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>{t('button.annuler')}</button>
|
||||
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>{t('button.confirmer')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
export function Input({placeholder, value, onChange}) {
|
||||
return <div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "./AxiosError.jsx";
|
||||
|
||||
function SimpleReducer(datas, action) {
|
||||
switch (action.type) {
|
||||
case 'ADD':
|
||||
return [
|
||||
...datas,
|
||||
action.payload
|
||||
]
|
||||
case 'REMOVE':
|
||||
return datas.filter(data => data.id !== action.payload)
|
||||
case 'UPDATE_OR_ADD':
|
||||
const index = datas.findIndex(data => data.id === action.payload.id)
|
||||
if (index === -1) {
|
||||
return [
|
||||
...datas,
|
||||
action.payload
|
||||
]
|
||||
} else {
|
||||
datas[index] = action.payload
|
||||
return [...datas]
|
||||
}
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
export function ListEditorTest() {
|
||||
const [html, dispatch] = ListEditor(ListHTML)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: {id: 1, content: "data in"}})
|
||||
}, []);
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
export function ListEditor(ListItem) {
|
||||
const [modal, setModal] = useState({id: -1})
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
|
||||
const sendAffiliation = (e) => {
|
||||
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: e})
|
||||
}
|
||||
|
||||
return [<>
|
||||
<ul className="list-group">
|
||||
{state.map((d, index) => {
|
||||
return <div key={index} className={"list-group-item d-flex justify-content-between align-items-start"}>
|
||||
<ListItem data={d}/>
|
||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||
data-bs-target="#EditModal" onClick={_ => setModal(d)}>
|
||||
<FontAwesomeIcon icon={faPen}/></button>
|
||||
<button className="badge btn btn-danger rounded-pill"
|
||||
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
|
||||
<FontAwesomeIcon icon={faTrashCan}/></button>
|
||||
</div>
|
||||
})}
|
||||
</ul>
|
||||
<div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
|
||||
aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={e => sendAffiliation(e, dispatch)}>
|
||||
<input name="id" value={modal.id} readOnly hidden/>
|
||||
</form>
|
||||
<ModalContent affiliation={modalAffiliation} dispatch={dispatch}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
, dispatch]
|
||||
}
|
||||
|
||||
function ListHTML({
|
||||
data
|
||||
}) {
|
||||
return <div className="me-auto">{data.content}</div>
|
||||
}
|
||||
@ -1,11 +1,15 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js";
|
||||
import {useCountries} from "../hooks/useCountries.jsx";
|
||||
import i18n from "../config/i18n.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function BirthDayField({inti_date, inti_category, required = true}) {
|
||||
const [date, setDate] = useState(inti_date)
|
||||
const [category, setCategory] = useState(inti_category)
|
||||
const [canUpdate, setCanUpdate] = useState(false)
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const b = category !== getCategoryFormBirthDate(new Date(date))
|
||||
if (b !== canUpdate)
|
||||
@ -18,19 +22,19 @@ export function BirthDayField({inti_date, inti_category, required = true}) {
|
||||
|
||||
return <>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="birth_date">Date de naissance</span>
|
||||
<span className="input-group-text" id="birth_date">{t('dateDeNaissance')}</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={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>
|
||||
<span className="input-group-text" id="category">{t('catégorie')}</span>
|
||||
<input type="text" className="form-control" placeholder="" name="category"
|
||||
aria-label="category" value={category ? getCatName(category) : ""} aria-describedby="category"
|
||||
disabled/>
|
||||
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
||||
onClick={updateCat}>Mettre à jours</button>}
|
||||
onClick={updateCat}>{t('mettreàJours')}</button>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -52,14 +56,14 @@ export function OptionField({name, text, values, value, disabled = false}) {
|
||||
export function RoleList({name, text, value, disabled = false}) {
|
||||
return <OptionField name={name} text={text} value={value} disabled={disabled}
|
||||
values={{
|
||||
MEMBRE: 'Membre',
|
||||
PRESIDENT: 'Président',
|
||||
TRESORIER: 'Trésorier',
|
||||
SECRETAIRE: 'Secrétaire',
|
||||
VPRESIDENT: 'Vise-Président',
|
||||
VTRESORIER: 'Vise-Trésorier',
|
||||
VSECRETAIRE: 'Vise-Secrétaire',
|
||||
MEMBREBUREAU: 'Membre bureau'
|
||||
MEMBRE: i18n.t('role.membre'),
|
||||
PRESIDENT: i18n.t('role.président'),
|
||||
TRESORIER: i18n.t('role.trésorier'),
|
||||
SECRETAIRE: i18n.t('role.secrétaire'),
|
||||
VPRESIDENT: i18n.t('role.vise-président'),
|
||||
VTRESORIER: i18n.t('role.vise-trésorier'),
|
||||
VSECRETAIRE: i18n.t('role.vise-secrétaire'),
|
||||
MEMBREBUREAU: i18n.t('role.membreDuBureau')
|
||||
}}/>
|
||||
}
|
||||
|
||||
|
||||
@ -3,14 +3,16 @@ import {NavLink} from "react-router-dom";
|
||||
import {useAuth} from "../hooks/useAuth.jsx";
|
||||
import {login, logout} from "../utils/auth.js";
|
||||
import {isClubAdmin} from "../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function Nav() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <nav className="navbar navbar-light navbar-expand-md bg-body-tertiary " id="main-navbar">
|
||||
<div className="container-fluid">
|
||||
<a className="navbar-brand" href="/">
|
||||
<img className="logo" src="/FFSSAF-bord-blanc-fond-transparent.webp" alt="logo"/>
|
||||
FFSAF Intranet
|
||||
{t("nav.title")}
|
||||
</a>
|
||||
|
||||
<button className="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
@ -21,7 +23,7 @@ export function Nav() {
|
||||
<div className="collapse navbar-collapse" id="navbarNavDropdown">
|
||||
<div className="collapse-item">
|
||||
<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="/">{t("nav.home")}</NavLink></li>
|
||||
<CompMenu/>
|
||||
<ClubMenu/>
|
||||
<AdminMenu/>
|
||||
@ -37,83 +39,87 @@ export function Nav() {
|
||||
|
||||
function AffiliationMenu() {
|
||||
const {is_authenticated} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (is_authenticated)
|
||||
return <></>
|
||||
return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">Demande d'affiliation</NavLink></li>
|
||||
return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">{t("nav.aff_request")}</NavLink></li>
|
||||
}
|
||||
|
||||
function CompMenu() {
|
||||
const {is_authenticated} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!is_authenticated)
|
||||
return <></>
|
||||
|
||||
return <li className="nav-item dropdown">
|
||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Compétitions
|
||||
{t("competition", {count: 2})}
|
||||
</div>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions
|
||||
Manager</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition">{t("registration", {count: 1})}</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/result">{t("nav.competitions.results")}</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">{t("comp_manage")}</NavLink></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
function ClubMenu() {
|
||||
const {is_authenticated, userinfo} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!is_authenticated || !(isClubAdmin(userinfo)))
|
||||
return <></>
|
||||
|
||||
return <li className="nav-item dropdown">
|
||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Club
|
||||
{t("club", {count: 1})}
|
||||
</div>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/club/me">Mon club</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Membres</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/club/me">{t("nav.club.my")}</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/club/member">{t("member", {count: 2})}</NavLink></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
function AdminMenu() {
|
||||
const {is_authenticated, userinfo} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
|
||||
return <></>
|
||||
|
||||
return <li className="nav-item dropdown">
|
||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Administration
|
||||
{t("admin")}
|
||||
</div>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Membres</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/club">Club</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/stats">Statistiques</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">{t("member", {count: 2})}</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/club">{t("club", {count: 2})}</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/admin/stats">{t("stats")}</NavLink></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
function LoginMenu() {
|
||||
const {is_authenticated} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{!is_authenticated ?
|
||||
<li className="nav-item">
|
||||
<div className="nav-link" onClick={() => login()}>Connexion</div>
|
||||
<div className="nav-link" onClick={() => login()}>{t("nav.login")}</div>
|
||||
</li>
|
||||
:
|
||||
<li className="nav-item dropdown">
|
||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Mon compte
|
||||
{t("nav.account")}
|
||||
</div>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/me">Mon espace</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/me">{t("nav.space")}</NavLink></li>
|
||||
<li className="nav-item">
|
||||
<div className="nav-link" onClick={() => logout()}>Déconnexion</div>
|
||||
<div className="nav-link" onClick={() => logout()}>{t("nav.logout")}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const removeDiacritics = str => {
|
||||
return str
|
||||
@ -9,6 +10,7 @@ const removeDiacritics = str => {
|
||||
|
||||
export function SearchBar({search, defaultValue = ""}) {
|
||||
const [searchInput, setSearchInput] = useState(defaultValue);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handelChange = (e) => {
|
||||
setSearchInput(e.target.value);
|
||||
@ -33,10 +35,10 @@ export function SearchBar({search, defaultValue = ""}) {
|
||||
|
||||
return <div className="mb-3">
|
||||
<div className="input-group mb-3">
|
||||
<input type="text" className="form-control" placeholder="Rechercher..." aria-label="Rechercher..."
|
||||
<input type="text" className="form-control" placeholder={t('rechercher...')} aria-label={t('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
|
||||
onClick={searchMember}>{t('rechercher')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
33
src/main/webapp/src/config/i18n.js
Normal file
33
src/main/webapp/src/config/i18n.js
Normal file
@ -0,0 +1,33 @@
|
||||
import i18n from 'i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import {initReactI18next} from 'react-i18next';
|
||||
|
||||
const options = {
|
||||
order: [ 'querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
|
||||
caches: [],
|
||||
}
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales
|
||||
// learn more: https://github.com/i18next/i18next-http-backend
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'fr',
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
detection: options,
|
||||
ns: ['common', 'result'],
|
||||
defaultNS: 'common',
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@ -2,6 +2,9 @@ import React, {lazy, Suspense} from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from "./App.jsx";
|
||||
|
||||
// import i18n (needs to be bundled ;))
|
||||
import './config/i18n.js';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {apiAxios, errFormater, getSaison} from "../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const notUpperCase = ["de", "la", "le", "les", "des", "du", "d'", "l'", "sur", 'lieu', 'dit'];
|
||||
|
||||
@ -48,6 +49,7 @@ export function DemandeAff() {
|
||||
const navigate = useNavigate();
|
||||
const [initData, setInitData] = useState(null)
|
||||
const [needFile, setNeedFile] = useState(true)
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (hash.startsWith("#d")) {
|
||||
@ -79,7 +81,7 @@ export function DemandeAff() {
|
||||
let error = false;
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
if (event.target[`m${i}_role`]?.value === "0") {
|
||||
toast.error(`Le rôle du membre ${i} est obligatoire`)
|
||||
toast.error(t('aff_req.error1', {i: i}));
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
@ -89,16 +91,7 @@ export function DemandeAff() {
|
||||
|
||||
if (event.nativeEvent.submitter.value === "undo") {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/affiliation/request/${initData.id}`),
|
||||
{
|
||||
pending: "Annulation de la demande d'affiliation en cours",
|
||||
success: "Demande d'affiliation annulée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'annulation de la demande d'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/affiliation/request/${initData.id}`), getToastMessage('aff_req.toast.undo')
|
||||
).then(_ => {
|
||||
navigate("/club/me")
|
||||
})
|
||||
@ -106,16 +99,7 @@ export function DemandeAff() {
|
||||
formData.append("id", initData.id)
|
||||
|
||||
toast.promise(
|
||||
apiAxios.put(`/affiliation/request/edit`, formData),
|
||||
{
|
||||
pending: "Enregistrement des modifications en cours",
|
||||
success: "Modifications enregistrées avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement des modifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.put(`/affiliation/request/edit`, formData), getToastMessage('toast.edit')
|
||||
).then(_ => {
|
||||
navigate("/club/me")
|
||||
})
|
||||
@ -123,16 +107,7 @@ export function DemandeAff() {
|
||||
formData.append("id", -1)
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/affiliation/request`, formData, {headers: {'Accept': '*/*'}}),
|
||||
{
|
||||
pending: "Enregistrement de la demande d'affiliation en cours",
|
||||
success: "Demande d'affiliation enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la demande d'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/affiliation/request`, formData, {headers: {'Accept': '*/*'}}), getToastMessage('aff.toast.save')
|
||||
).then(_ => {
|
||||
navigate("/affiliation/ok")
|
||||
})
|
||||
@ -140,62 +115,46 @@ export function DemandeAff() {
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>Demande d'affiliation {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}</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 :
|
||||
<h1>{t("nav.aff_request")} {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}</h1>
|
||||
<p>{t('aff_req.text1')}</p>
|
||||
{t('aff_req.text2')}
|
||||
<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>
|
||||
{t('aff_req.text2.li', {returnObjects: true}).map((text, index) => <li key={index}>{text}</li>)}
|
||||
</ul>
|
||||
|
||||
{initData && <div className="card mb-4">
|
||||
<form onSubmit={submit}>
|
||||
<div className="card-body">
|
||||
<h4>L'association</h4>
|
||||
<h4>{t('aff_req.association')}</h4>
|
||||
<AssoInfo initData={initData} needFile={needFile}/>
|
||||
<h4>Membre n°1</h4>
|
||||
<h4>{t("aff.membreNo", {no: 1})}</h4>
|
||||
<MembreInfo role="m1" initData={initData.members?.at(0) || {}}/>
|
||||
<h4 style={{marginTop: '1em'}}>Membre n°2</h4>
|
||||
<h4 style={{marginTop: '1em'}}>{t("aff.membreNo", {no: 2})}</h4>
|
||||
<MembreInfo role="m2" initData={initData.members?.at(1) || {}}/>
|
||||
<h4 style={{marginTop: '1em'}}>Membre n°3</h4>
|
||||
<h4 style={{marginTop: '1em'}}>{t("aff.membreNo", {no: 3})}</h4>
|
||||
<MembreInfo role="m3" initData={initData.members?.at(2) || {}}/>
|
||||
|
||||
<div className="mb-3" style={{marginTop: '1em'}}>
|
||||
{!initData.id && <p>Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour
|
||||
accéder à votre espace FFSAF</p>}
|
||||
Notez que pour finaliser votre affiliation, il vous faudra :
|
||||
{!initData.id && <p>{t('aff_req.text3')}</p>}
|
||||
{t('aff_req.text4')}
|
||||
<ul>
|
||||
<li>Disposer d’au moins trois membres licenciés, dont le président</li>
|
||||
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li>
|
||||
<li>{t('aff_req.text4.li1')}</li>
|
||||
<li>{t('aff_req.text4.li2')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{!initData.id ?
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button type="submit" value="new" className="btn btn-primary">Confirmer ma demande d'affiliation
|
||||
</button>
|
||||
<button type="submit" value="new" className="btn btn-primary">{t('aff_req.button.confirm')}</button>
|
||||
</div>
|
||||
</div> :
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-center col">
|
||||
<button type="submit" value="undo" className="btn btn-danger">Annuler ma demande
|
||||
</button>
|
||||
<button type="submit" value="undo" className="btn btn-danger">{t('aff_req.button.cancel')}</button>
|
||||
</div>
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-center col">
|
||||
<button type="submit" value="edit" className="btn btn-primary">Enregistrer les modifications
|
||||
</button>
|
||||
<button type="submit" value="edit" className="btn btn-primary">{t('aff_req.button.save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -210,12 +169,13 @@ function AssoInfo({initData, needFile}) {
|
||||
const [stateId, setStateId] = useState(initData.stateId ? String(initData.stateId) : (initData.state_id ? String(initData.state_id) : ""))
|
||||
const [adresse, setAdresse] = useState(initData.address ? initData.address : "")
|
||||
const [contact, setContact] = useState(initData.contact ? initData.contact : "")
|
||||
const {t} = useTranslation();
|
||||
|
||||
const fetchStateId = () => {
|
||||
const regex = /^(?:\d{14}|W\d{9})$/;
|
||||
let sid = stateId;
|
||||
if (!regex.test(stateId)) {
|
||||
toast.error("Le format du SIRET/RNA est invalide");
|
||||
toast.error(t('aff_req.error2'));
|
||||
return;
|
||||
} else {
|
||||
if (stateId[0] !== 'W')
|
||||
@ -223,16 +183,7 @@ function AssoInfo({initData, needFile}) {
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.get(`asso/state_id/${sid}`),
|
||||
{
|
||||
pending: "Recherche de l'association en cours",
|
||||
success: "Association trouvée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la recherche de l'association")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.get(`asso/state_id/${sid}`), getToastMessage("aff_req.toast.search")
|
||||
).then(data => {
|
||||
const data2 = data.data
|
||||
setDenomination(data2.identite.nom)
|
||||
@ -244,44 +195,42 @@ function AssoInfo({initData, needFile}) {
|
||||
<input name="saison" value={getSaisonToAff()} readOnly hidden/>
|
||||
|
||||
<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"
|
||||
<span className="input-group-text" id="basic-addon1">{t('aff_req.nomDeLassociation')}*</span>
|
||||
<input type="text" className="form-control" placeholder={t('aff_req.nomDeLassociation')} name="name"
|
||||
aria-label={t('aff_req.nomDeLassociation')}
|
||||
aria-describedby="basic-addon1" required defaultValue={initData.name ? initData.name : ""}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">N° SIRET ou RNA*</span>
|
||||
<input type="text" className="form-control" placeholder="N° SIRET ou RNA*" name="state_id" required value={stateId} disabled={!needFile}
|
||||
<span className="input-group-text">{t('noSiretOuRna')}*</span>
|
||||
<input type="text" className="form-control" placeholder={t('noSiretOuRna')} name="state_id" required value={stateId} disabled={!needFile}
|
||||
onChange={e => setStateId(e.target.value)}/>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
|
||||
onClick={fetchStateId} hidden={false}>Rechercher
|
||||
onClick={fetchStateId} hidden={false}>{t('rechercher')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={false}>
|
||||
<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"
|
||||
<span className="input-group-text" id="basic-addon1">{t('aff_req.denomination')}</span>
|
||||
<input type="text" className="form-control" placeholder={t('aff_req.appuyerSurRechercher')}
|
||||
aria-label={t('aff_req.denomination')}
|
||||
aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<span className="input-group-text" id="basic-addon1">Adresse administrative*</span>
|
||||
<input type="text" className="form-control" placeholder="Adresse administrative" aria-label="Adresse administrative"
|
||||
<span className="input-group-text" id="basic-addon1">{t('adresseAdministrative')}*</span>
|
||||
<input type="text" className="form-control" placeholder={t('adresseAdministrative')} aria-label={t('adresseAdministrative')}
|
||||
aria-describedby="basic-addon1"
|
||||
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
|
||||
</div>
|
||||
<div className="form-text" id="adresse">Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux
|
||||
d'entrainement
|
||||
</div>
|
||||
<div className="form-text" id="adresse">{t('aff_req.text5')}</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<span className="input-group-text" id="basic-addon1">Contact administratif*</span>
|
||||
<input type="email" className="form-control" placeholder="Contact administratif" aria-label="Contact administratif"
|
||||
<span className="input-group-text" id="basic-addon1">{t('contactAdministratif')}*</span>
|
||||
<input type="email" className="form-control" placeholder={t('contactAdministratif')} aria-label={t('contactAdministratif')}
|
||||
aria-describedby="basic-addon1"
|
||||
required value={contact} name="contact" onChange={e => setContact(e.target.value)}/>
|
||||
</div>
|
||||
@ -289,70 +238,69 @@ function AssoInfo({initData, needFile}) {
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="logo">Blason{needFile && "*"}</label>
|
||||
<label className="input-group-text" htmlFor="logo">{t('blason')}{needFile && "*"}</label>
|
||||
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg"
|
||||
required={needFile}/>
|
||||
</div>
|
||||
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette
|
||||
demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)</div>}
|
||||
{!needFile && <div className="form-text" id="status">{t('aff_req.text6')}</div>}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="status">Statuts{needFile && "*"}</label>
|
||||
<label className="input-group-text" htmlFor="status">{t('statuts')}{needFile && "*"}</label>
|
||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/>
|
||||
</div>
|
||||
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette
|
||||
demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)</div>}
|
||||
{!needFile && <div className="form-text" id="status">{t('aff_req.text7')}</div>}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
function MembreInfo({role, initData}) {
|
||||
const [switchOn, setSwitchOn] = useState(!!initData.licence);
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôle</label>
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">{t('role')}</label>
|
||||
<select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)}
|
||||
disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required>
|
||||
<option value="0">Sélectionner...</option>
|
||||
<option value="PRESIDENT">Président</option>
|
||||
<option value="TRESORIER">Trésorier</option>
|
||||
<option value="SECRETAIRE">Secrétaire</option>
|
||||
<option value="VPRESIDENT">Vise-Président</option>
|
||||
<option value="VTRESORIER">Vise-Trésorier</option>
|
||||
<option value="VSECRETAIRE">Vise-Secrétaire</option>
|
||||
<option value="MEMBREBUREAU">Membre du bureau</option>
|
||||
<option value="0">{t('selectionner...')}</option>
|
||||
<option value="PRESIDENT">{t('role.président')}</option>
|
||||
<option value="TRESORIER">{t('role.trésorier')}</option>
|
||||
<option value="SECRETAIRE">{t('role.secrétaire')}</option>
|
||||
<option value="VPRESIDENT">{t('role.vise-président')}</option>
|
||||
<option value="VTRESORIER">{t('role.vise-trésorier')}</option>
|
||||
<option value="VSECRETAIRE">{t('role.vise-secrétaire')}</option>
|
||||
<option value="MEMBREBUREAU">{t('role.membreDuBureau')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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"}
|
||||
<input type="text" className="form-control" id="floatingInput" placeholder={t('nom')} name={role + "_nom"}
|
||||
defaultValue={initData.lname ? initData.lname : ""} required/>
|
||||
<label htmlFor="floatingInput">Nom*</label>
|
||||
<label htmlFor="floatingInput">{t('nom')}*</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<div className="form-floating">
|
||||
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom"
|
||||
<input type="text" className="form-control" id="floatingInput" placeholder={t('prenom')}
|
||||
name={role + "_prenom"} defaultValue={initData.fname ? initData.fname : ""} required/>
|
||||
<label htmlFor="floatingInput">Prénom*</label>
|
||||
<label htmlFor="floatingInput">{t('prenom')}*</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"} defaultValue={initData.email ? initData.email : ""} required/>
|
||||
<label htmlFor="floatingInput">Email*</label>
|
||||
<label htmlFor="floatingInput">{t('email')}*</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">Dispose déjà d'une licence</label>
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">{t('aff_req.disposeLicence')}</label>
|
||||
<div className="input-group-text">
|
||||
<input type="checkbox" id="inputGroupSelect01" className="form-check-input mt-0"
|
||||
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
|
||||
@ -361,9 +309,9 @@ function MembreInfo({role, initData}) {
|
||||
{switchOn &&
|
||||
<div className="col-sm-3">
|
||||
<div className="form-floating">
|
||||
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
|
||||
<input type="number" className="form-control" id="floatingInput" placeholder={t('noLicence')}
|
||||
name={role + "_licence"} defaultValue={initData.licence ? Number(initData.licence) : ""} required/>
|
||||
<label htmlFor="floatingInput">N° Licence</label>
|
||||
<label htmlFor="floatingInput">{t('noLicence')}</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -372,11 +320,12 @@ function MembreInfo({role, initData}) {
|
||||
}
|
||||
|
||||
export function DemandeAffOk() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
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 identifiant et mot de passe provisoire pour accéder à votre
|
||||
espace FFSAF</p>
|
||||
<h1 className="text-green-800 text-4xl">{t('aff_req.text7')}</h1>
|
||||
<p>{t('aff_req.text8')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,42 +1,31 @@
|
||||
import {faUser, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
export const Home = () => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="container">
|
||||
<div style={{textAlign: "center", margin: "2em"}}>
|
||||
<h1 className="text-green-800 text-4xl">Bienvenue sur l’intranet de la Fédération France Soft Armored Fighting</h1>
|
||||
<h1 className="text-green-800 text-4xl">{t("home.welcome_message")}</h1>
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "3em"}}>
|
||||
<div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}>
|
||||
<div className="align-content-center"
|
||||
style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}>
|
||||
<h2><FontAwesomeIcon icon={faUser} size="2xl"/></h2>
|
||||
<h2>Pour les licenciés</h2>
|
||||
<h2>{t("home.header1")}</h2>
|
||||
</div>
|
||||
<p style={{padding: "0.5em 1em 0.5em 1em"}}>
|
||||
Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également
|
||||
télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que
|
||||
le club organisateur les ait renseignés. <br/>
|
||||
<br/>
|
||||
Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé
|
||||
une fois votre licence validée par le secrétariat.
|
||||
</p>
|
||||
<p style={{padding: "0.5em 1em 0.5em 1em"}}><Trans i18nKey="home.text1"></Trans></p>
|
||||
</div>
|
||||
<div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}>
|
||||
<div className="align-content-center"
|
||||
style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}>
|
||||
<h2><FontAwesomeIcon icon={faUsers} size="2xl"/></h2>
|
||||
<h2>Pour les clubs</h2>
|
||||
<h2>{t("home.header2")}</h2>
|
||||
</div>
|
||||
<p style={{padding: "0.5em 1em 0.5em 1em"}}>
|
||||
C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre
|
||||
affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur
|
||||
le site ffsaf.fr.<br/>
|
||||
Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi
|
||||
que d'enregistrer les résultats.<br/><br/>
|
||||
Vous n'êtes pas encore affilié à la fédération ? Cliquez <a href="/affiliation">içi</a> pour faire votre première demande.
|
||||
</p>
|
||||
<p style={{padding: "0.5em 1em 0.5em 1em"}}><Trans i18nKey="home.text2">Cliquez <a href="/affiliation">içi</a> pour </Trans></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -12,19 +12,20 @@ import {
|
||||
faUserGroup,
|
||||
faVenus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {CheckField} from "../components/MemberCustomFiels.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, getCatName} from "../utils/Tools.js";
|
||||
import {apiAxios, getCatName, getToastMessage} from "../utils/Tools.js";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function MePage() {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/member/me`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div>
|
||||
<h1>Mon espace</h1>
|
||||
<h1>{t("nav.space")}</h1>
|
||||
|
||||
{data
|
||||
? <div>
|
||||
@ -52,10 +53,12 @@ export function MePage() {
|
||||
}
|
||||
|
||||
export function LicenceCard({userData}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
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">{t('licence')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
@ -73,8 +76,10 @@ export function LicenceCard({userData}) {
|
||||
}
|
||||
|
||||
function PhotoCard({data}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Licence n°{data.licence}</div>
|
||||
<div className="card-header">{t("licenceNo", {no: data.licence})}</div>
|
||||
<div className="card-body text-center">
|
||||
<div className="input-group mb-3">
|
||||
<img
|
||||
@ -85,8 +90,8 @@ function PhotoCard({data}) {
|
||||
|
||||
<a href={`${vite_url}/api/member/me/licence`} target='#'>
|
||||
<button className="btn btn-primary" type="button" id="button-addon1"
|
||||
onClick={e => null}>
|
||||
Téléchargée la licence <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
onClick={() => null}>
|
||||
{t('téléchargéeLaLicence')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
@ -94,13 +99,15 @@ function PhotoCard({data}) {
|
||||
}
|
||||
|
||||
function SelectCard({userData}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header">Sélection en équipe de France</div>
|
||||
<div className="card-header">{t('sélectionEnéquipeDeFrance')}</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{userData?.selections && userData.selections.sort((a, b) => b.saison - a.saison).map((selection, index) => {
|
||||
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} {t('en')} {getCatName(selection?.categorie)}</div>
|
||||
</div>
|
||||
})}
|
||||
</ul>
|
||||
@ -110,6 +117,7 @@ function SelectCard({userData}) {
|
||||
|
||||
function SettingsCard({data}) {
|
||||
const [privacy, setPrivacy] = useState("PUBLIC");
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.resultPrivacy) {
|
||||
@ -120,28 +128,22 @@ function SettingsCard({data}) {
|
||||
const handleChange = (e) => {
|
||||
const formData = new FormData();
|
||||
formData.append("resultPrivacy", e.target.value);
|
||||
toast.promise(apiAxios.put(`/member/me/setting`, formData),
|
||||
{
|
||||
pending: 'Mise à jours des paramètres en cours...',
|
||||
success: 'Paramètres mis à jours avec succès 🎉',
|
||||
error: 'Échec de la mise à jours des paramètres 😕'
|
||||
})
|
||||
toast.promise(apiAxios.put(`/member/me/setting`, formData), getToastMessage("me.toast.settings"))
|
||||
.then(() => {
|
||||
setPrivacy(String(formData.get("resultPrivacy")));
|
||||
});
|
||||
}
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Paramètres du compte</div>
|
||||
<div className="card-header">{t('me.paramètresDuCompte')}</div>
|
||||
<div className="card-body">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="email_notifications">Visibilité des résultats</label>
|
||||
<label className="input-group-text" htmlFor="email_notifications">{t('me.visibilitéDesRésultats')}</label>
|
||||
<select className="form-select" id="result_visibility" name="result_visibility" required value={privacy} onChange={handleChange}>
|
||||
<option value="PUBLIC">Public (visible par tous)</option>
|
||||
<option value="REGISTERED_ONLY">Membres connectés (visibles par les membres de la fédération)</option>
|
||||
<option value="REGISTERED_ONLY_NO_DETAILS">Membres connectés - masquer les détails (visibles par les membres de la fédération)
|
||||
</option>
|
||||
<option value="PRIVATE">Privé (visible uniquement par moi)</option>
|
||||
<option value="PUBLIC">{t('me.result.PUBLIC')}</option>
|
||||
<option value="REGISTERED_ONLY">{t('me.result.REGISTERED_ONLY')}</option>
|
||||
<option value="REGISTERED_ONLY_NO_DETAILS">{t('me.result.REGISTERED_ONLY_NO_DETAILS')}</option>
|
||||
<option value="PRIVATE">{t('me.result.PRIVATE')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -150,9 +152,10 @@ function SettingsCard({data}) {
|
||||
|
||||
function InformationForm({data}) {
|
||||
const style = {marginRight: '0.7em'}
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
<div className="card-header">{t('information')}</div>
|
||||
<div className="card-body">
|
||||
<div className="row mb-2">
|
||||
<p>
|
||||
@ -163,16 +166,16 @@ function InformationForm({data}) {
|
||||
|| <FontAwesomeIcon icon={faMarsAndVenus} style={style}/>}{data.genre}<br/>
|
||||
<FontAwesomeIcon icon={faCalendarDay} style={style}/>{data.birth_date ? data.birth_date.split('T')[0] : ''}<br/>
|
||||
<FontAwesomeIcon icon={faUserGroup} style={style}/>{data.categorie}<br/>
|
||||
<FontAwesomeIcon icon={faFlag} style={style}/>Nationalité : <img src={"/flags/flags_" + data.country.toLowerCase() + ".png"}
|
||||
<FontAwesomeIcon icon={faFlag} style={style}/>{t('nationalité')} : <img src={"/flags/flags_" + data.country.toLowerCase() + ".png"}
|
||||
alt=""/><br/>
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={style}/>Club : {data.club}<br/>
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={style}/>Rôle au sien du club : {data.role}<br/>
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={style}/>Formation d'arbitrage : {data.grade_arbitrage}
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={style}/>{t("club", {count: 1})} : {data.club}<br/>
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={style}/>{t('me.rôleAuSienDuClub')} : {data.role}<br/>
|
||||
<FontAwesomeIcon icon={faInfoCircle} style={style}/>{t('me.formationDarbitrage')} : {data.grade_arbitrage}
|
||||
</p>
|
||||
<div>
|
||||
<button className="btn btn-primary" type="button" id="button-addon1"
|
||||
onClick={_ => window.location.href = "https://auth.ffsaf.fr/realms/ffsaf/login-actions/reset-credentials?client_id=ffsaf-client"}>
|
||||
Changer mon mot de passe
|
||||
{t('me.changerMonMotDePasse')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,12 +6,13 @@ import {useEffect, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {Checkbox} from "../components/MemberCustomFiels.jsx";
|
||||
import * as Tools from "../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
|
||||
import {apiAxios, getCatName, getToastMessage} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SearchBar} from "../components/SearchBar.jsx";
|
||||
import * as XLSX from "xlsx-js-style";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
let lastRefresh = "";
|
||||
@ -19,6 +20,7 @@ let lastRefresh = "";
|
||||
export function MemberList({source}) {
|
||||
const {hash} = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [memberData, setMemberData] = useState([]);
|
||||
const [licenceData, setLicenceData] = useState([]);
|
||||
@ -82,16 +84,7 @@ export function MemberList({source}) {
|
||||
return;
|
||||
|
||||
toast.promise(
|
||||
apiAxios.get(`/licence/current/${source}`),
|
||||
{
|
||||
pending: "Chargement des licences...",
|
||||
success: "Licences chargées",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Impossible de charger les licences")
|
||||
}
|
||||
}
|
||||
})
|
||||
apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
|
||||
.then(data => {
|
||||
setLicenceData(data.data);
|
||||
});
|
||||
@ -118,24 +111,24 @@ export function MemberList({source}) {
|
||||
</div>
|
||||
<div className="col-lg-3">
|
||||
<div className="mb-4">
|
||||
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un membre</button>
|
||||
<button className="btn btn-primary" onClick={() => navigate("new")}>{t('ajouterUnMembre')}</button>
|
||||
{source === "admin" &&
|
||||
<button className="btn btn-primary" onClick={() => navigate("validate")} style={{marginTop: "0.5rem"}}>Valider des
|
||||
licences</button>}
|
||||
<button className="btn btn-primary" onClick={() => navigate("validate")}
|
||||
style={{marginTop: "0.5rem"}}>{t('validerDesLicences')}</button>}
|
||||
{source === "club" && false && // TODO: enable when payment is ready
|
||||
<button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des
|
||||
licences</button>}
|
||||
<button className="btn btn-primary" onClick={() => navigate("pay")}
|
||||
style={{marginTop: "0.5rem"}}>{t('paiementDesLicences')}</button>}
|
||||
</div>
|
||||
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Trie</div>
|
||||
<div className="card-header">{t('trie')}</div>
|
||||
<div className="card-body">
|
||||
<OrderBar onOrderChange={e => setFilter({...filter, order: e.join(",")})} defaultValues={filter.order} source={source}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar showLicenceState={showLicenceState}
|
||||
setShowLicenceState={setShowLicenceState}
|
||||
@ -155,7 +148,7 @@ export function MemberList({source}) {
|
||||
|
||||
{source === "club" &&
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Gestion groupée</div>
|
||||
<div className="card-header">{t('gestionGroupée')}</div>
|
||||
<div className="card-body">
|
||||
<FileOutput/>
|
||||
<div style={{marginTop: "1.5em"}}></div>
|
||||
@ -169,6 +162,7 @@ export function MemberList({source}) {
|
||||
}
|
||||
|
||||
function FileOutput() {
|
||||
const {t} = useTranslation();
|
||||
function formatColumnDate(worksheet, col) {
|
||||
const range = XLSX.utils.decode_range(worksheet['!ref'])
|
||||
// note: range.s.r + 1 skips the header row
|
||||
@ -185,16 +179,7 @@ function FileOutput() {
|
||||
|
||||
const handleFileDownload = () => {
|
||||
toast.promise(
|
||||
apiAxios.get(`/member/club/export`),
|
||||
{
|
||||
pending: "Exportation des licences...",
|
||||
success: "Licences exportées",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Impossible d'exporté les licences")
|
||||
}
|
||||
}
|
||||
})
|
||||
apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export"))
|
||||
.then(data => {
|
||||
const dataOut = []
|
||||
for (const e of data.data) {
|
||||
@ -241,14 +226,15 @@ function FileOutput() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className="btn btn-primary" onClick={handleFileDownload}>Télécharger l'Excel des membres</button>
|
||||
<small>À utiliser comme template pour mettre à jour les informations</small>
|
||||
<button className="btn btn-primary" onClick={handleFileDownload}>{t('téléchargerLexcelDesMembres')}</button>
|
||||
<small>{t('téléchargerLexcelDesMembres.info')}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function FileInput() {
|
||||
const {t} = useTranslation();
|
||||
const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
|
||||
|
||||
function excelDateToJSDate(serial) {
|
||||
@ -284,21 +270,21 @@ function FileInput() {
|
||||
}
|
||||
|
||||
if (tmp.nom === undefined || tmp.nom === "") {
|
||||
toast.error("Nom vide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.nomVideàLaLigne', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
if (tmp.prenom === undefined || tmp.prenom === "") {
|
||||
toast.error("Prénom vide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.prénomVideàLaLigne', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
|
||||
if (tmp.licenceCurrent) { // need check full data
|
||||
if (tmp.email === undefined || tmp.email === "") {
|
||||
toast.error("Email vide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.emailVideàLaLigne', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
if (!re.test(tmp.email)) {
|
||||
toast.error("Email invalide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.import.err5', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
// noinspection NonAsciiCharacters,JSNonASCIINames
|
||||
@ -309,20 +295,20 @@ function FileInput() {
|
||||
try {
|
||||
const date = excelDateToJSDate(line["Date certificat"]);
|
||||
if (Number.isNaN(date.getFullYear())) {
|
||||
toast.error("Format de la date de certificat invalide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.import.err1', {no: i + 2}))
|
||||
error++;
|
||||
} else {
|
||||
// noinspection JSNonASCIINames
|
||||
tmp.certif = line["Nom médecin certificat"] + "¤" + date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error("Format de la date de certificat invalide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.import.err2', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp.birthdate === undefined || tmp.birthdate === "") {
|
||||
toast.error("Date de naissance vide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.import.err3', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
}
|
||||
@ -332,7 +318,7 @@ function FileInput() {
|
||||
try {
|
||||
tmp.birthdate = excelDateToJSDate(tmp.birthdate).toISOString();
|
||||
} catch (e) {
|
||||
toast.error("Format de la date de naissance invalide à la ligne " + (i + 2))
|
||||
toast.error(t('membre.import.err4', {no: i + 2}))
|
||||
error++;
|
||||
}
|
||||
}
|
||||
@ -341,24 +327,15 @@ function FileInput() {
|
||||
}
|
||||
|
||||
if (error > 0) {
|
||||
toast.error(`${error} erreur(s) dans le fichier, opération annulée`)
|
||||
toast.error(t('membre.import.errTT', {count: error}))
|
||||
} else {
|
||||
console.log(dataOut);
|
||||
|
||||
toast.promise(
|
||||
apiAxios.put(`/member/club/import`, dataOut),
|
||||
{
|
||||
pending: "Envoie des changement en cours",
|
||||
success: "Changement envoyé avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'envoie des changements")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.put(`/member/club/import`, dataOut), getToastMessage("membre.toast.licences.import")
|
||||
).then(_ => {
|
||||
if (cetifNotFill > 0)
|
||||
toast.warn(`${cetifNotFill} certificat(s) médical(aux) non rempli(s)`)
|
||||
toast.warn(t('membre.import.warn', {count: cetifNotFill}))
|
||||
})
|
||||
}
|
||||
};
|
||||
@ -368,16 +345,18 @@ function FileInput() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>Charger l'Excel</span>
|
||||
<span>{t('chargerLexcel')}</span>
|
||||
<div className="input-group">
|
||||
<input type="file" className="form-control" id="logo" name="logo" accept=".xls,.xlsx" onChange={handleFileUpload}/>
|
||||
</div>
|
||||
<small>Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.</small>
|
||||
<small>{t('chargerLexcel.msg')}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const pages = []
|
||||
for (let i = 1; i <= data.page_count; i++) {
|
||||
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
|
||||
@ -387,8 +366,12 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
|
||||
|
||||
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>
|
||||
<small>{t("page_info_full", {
|
||||
line: ((page - 1) * data.page_size) + 1,
|
||||
tt_line: (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size),
|
||||
page: page,
|
||||
tt_page: data.page_count,
|
||||
})}</small>
|
||||
<div className="list-group">
|
||||
{visibleMember.map(member => (
|
||||
<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))}
|
||||
@ -453,6 +436,7 @@ function MakeRow({member, showLicenceState, navigate, source}) {
|
||||
}
|
||||
|
||||
function OrderBar({onOrderChange, defaultValues = "", source}) {
|
||||
const {t} = useTranslation();
|
||||
const [orderCriteria, setOrderCriteria] = useState([...defaultValues.split(",").filter(c => c !== ''), '']);
|
||||
|
||||
const handleChange = (index, value) => {
|
||||
@ -474,20 +458,20 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
|
||||
|
||||
// Liste de toutes les options possibles
|
||||
const allOptions = [
|
||||
{value: 'lname n', label: 'Nom ↓', base: 'lname'},
|
||||
{value: 'lname i', label: 'Nom ↑', base: 'lname'},
|
||||
{value: 'fname n', label: 'Prénom ↓', base: 'fname'},
|
||||
{value: 'fname i', label: 'Prénom ↑', base: 'fname'},
|
||||
{value: 'categorie n', label: 'Catégorie ↓', base: 'categorie'},
|
||||
{value: 'categorie i', label: 'Catégorie ↑', base: 'categorie'},
|
||||
{value: 'licence n', label: 'Licence ↓', base: 'licence'},
|
||||
{value: 'licence i', label: 'Licence ↑', base: 'licence'},
|
||||
{value: 'lname n', label: t('nom') + ' ↓', base: 'lname'},
|
||||
{value: 'lname i', label: t('nom') + ' ↑', base: 'lname'},
|
||||
{value: 'fname n', label: t('prenom') + ' ↓', base: 'fname'},
|
||||
{value: 'fname i', label: t('prenom') + ' ↑', base: 'fname'},
|
||||
{value: 'categorie n', label: t('catégorie') + ' ↓', base: 'categorie'},
|
||||
{value: 'categorie i', label: t('catégorie') + ' ↑', base: 'categorie'},
|
||||
{value: 'licence n', label: t('licence') + ' ↓', base: 'licence'},
|
||||
{value: 'licence i', label: t('licence') + ' ↑', base: 'licence'},
|
||||
];
|
||||
|
||||
if (source === "admin") {
|
||||
allOptions.push(
|
||||
{value: 'club.name n', label: 'Club ↓', base: 'club.name'},
|
||||
{value: 'club.name i', label: 'Club ↑', base: 'club.name'},
|
||||
{value: 'club.name n', label: t('club', {count: 1}) + ' ↓', base: 'club.name'},
|
||||
{value: 'club.name i', label: t('club', {count: 1}) + ' ↑', base: 'club.name'},
|
||||
);
|
||||
}
|
||||
|
||||
@ -539,17 +523,18 @@ function FiltreBar({
|
||||
catFilter,
|
||||
setCatFilter,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
|
||||
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label={t('membre.filtre.licence')}/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label="Afficher les combattants inactifs"/>
|
||||
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
||||
<option value="">--- toute les catégories ---</option>
|
||||
<option value="">{t('---TouteLesCatégories---')}</option>
|
||||
{Tools.CatList.map(cat => (
|
||||
<option key={cat} value={cat}>{getCatName(cat)}</option>
|
||||
))}
|
||||
@ -558,21 +543,21 @@ function FiltreBar({
|
||||
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
|
||||
<option value={4}>Tout les états de licences</option>
|
||||
<option value={0}>Sans demande ni licence validée</option>
|
||||
<option value={1}>Avec demande ou licence validée</option>
|
||||
<option value={2}>Demande en cours</option>
|
||||
<option value={5}>Demande complet</option>
|
||||
<option value={6}>Demande incomplet</option>
|
||||
<option value={3}>Licence validée</option>
|
||||
<option value={4}>{t('membre.filtre.licences.4')}</option>
|
||||
<option value={0}>{t('membre.filtre.licences.0')}</option>
|
||||
<option value={1}>{t('membre.filtre.licences.1')}</option>
|
||||
<option value={2}>{t('membre.filtre.licences.2')}</option>
|
||||
<option value={5}>{t('membre.filtre.licences.5')}</option>
|
||||
<option value={6}>{t('membre.filtre.licences.6')}</option>
|
||||
<option value={3}>{t('membre.filtre.licences.3')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}
|
||||
hidden={stateFilter === 0 || stateFilter === 4}>
|
||||
<option value={2}>Tout les états de paiement</option>
|
||||
<option value={0}>Sans paiement</option>
|
||||
<option value={1}>Avec paiement</option>
|
||||
<option value={2}>{t('membre.filtre.payement.2')}</option>
|
||||
<option value={0}>{t('membre.filtre.payement.0')}</option>
|
||||
<option value={1}>{t('membre.filtre.payement.1')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -581,13 +566,14 @@ function FiltreBar({
|
||||
function ClubSelectFilter({clubFilter, setClubFilter}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{data
|
||||
? <div className="mb-3">
|
||||
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
||||
<option value="">--- tout les clubs ---</option>
|
||||
<option value="null">--- sans club ---</option>
|
||||
<option value="">{t('---ToutLesClubs---')}</option>
|
||||
<option value="null">{t('---SansClub---')}</option>
|
||||
{data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -4,7 +4,7 @@ import {AxiosError} from "../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
|
||||
import {apiAxios, getCatName, getToastMessage} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SearchBar} from "../components/SearchBar.jsx";
|
||||
import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
|
||||
@ -12,8 +12,11 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./PayAndValidateList.css";
|
||||
import * as Tools from "../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {counter} from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
export function PayAndValidateList({source}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {hash} = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@ -66,16 +69,7 @@ export function PayAndValidateList({source}) {
|
||||
|
||||
const fetchLicenceData = () => {
|
||||
toast.promise(
|
||||
apiAxios.get(`/licence/current/${source}`),
|
||||
{
|
||||
pending: "Chargement des licences...",
|
||||
success: "Licences chargées",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Impossible de charger les licences")
|
||||
}
|
||||
}
|
||||
})
|
||||
apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
|
||||
.then(data => {
|
||||
setLicenceData(data.data);
|
||||
});
|
||||
@ -93,21 +87,12 @@ export function PayAndValidateList({source}) {
|
||||
|
||||
const handleValidation = () => {
|
||||
if (selectedMembers.length === 0) {
|
||||
toast.error("Aucun membre sélectionné");
|
||||
toast.error(t('aucunMembreSélectionné'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/validate`, selectedMembers),
|
||||
{
|
||||
pending: "Validation des licences en cours...",
|
||||
success: "Licences validées avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la validation des licences")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/licence/validate`, selectedMembers), getToastMessage("toast.licence.bulk.valid")
|
||||
).then(() => {
|
||||
setSelectedMembers([]);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
|
||||
@ -116,21 +101,12 @@ export function PayAndValidateList({source}) {
|
||||
|
||||
const handlePayment = () => {
|
||||
if (selectedMembers.length === 0) {
|
||||
toast.error("Aucun membre sélectionné");
|
||||
toast.error(t('aucunMembreSélectionné'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/validate-pay`, selectedMembers),
|
||||
{
|
||||
pending: "Validation des licences en cours...",
|
||||
success: "Licences validées avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la validation des licences")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/licence/validate-pay`, selectedMembers), getToastMessage("toast.licence.bulk.pay")
|
||||
).then(() => {
|
||||
setSelectedMembers([]);
|
||||
fetchLicenceData();
|
||||
@ -145,16 +121,7 @@ export function PayAndValidateList({source}) {
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/pay`, selectedMembers),
|
||||
{
|
||||
pending: "Création de la commande en cours...",
|
||||
success: "Commande crée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de le création de la commande")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/licence/pay`, selectedMembers), getToastMessage("toast.licence.order")
|
||||
).then((data) => {
|
||||
window.location.href = data.data;
|
||||
});
|
||||
@ -163,7 +130,7 @@ export function PayAndValidateList({source}) {
|
||||
return <>
|
||||
<h2>Validation des licences</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("../member")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
@ -179,7 +146,7 @@ export function PayAndValidateList({source}) {
|
||||
</div>
|
||||
<div className="col-lg-3">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
|
||||
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
|
||||
@ -189,38 +156,34 @@ export function PayAndValidateList({source}) {
|
||||
|
||||
<div className="mb-4">
|
||||
{source === "admin" && <>
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-pay">Valider le payement
|
||||
des {selectedMembers.length} licences sélectionnée
|
||||
<button className="btn btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#confirm-pay">{t('validerLePayement', {count: selectedMembers.length})}
|
||||
</button>
|
||||
<ConfirmDialog title="Payment des licences"
|
||||
message={"Êtes-vous sûr de vouloir marquer comme payées les " + selectedMembers.length + " licences ?"}
|
||||
<ConfirmDialog title={t('paymentDesLicences')}
|
||||
message={t('paymentDesLicences.msg', {count: selectedMembers.length})}
|
||||
onConfirm={handlePayment} id="confirm-pay"/>
|
||||
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation" style={{marginTop: "0.5em"}}>Valider
|
||||
les {selectedMembers.length} licences sélectionnée
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation"
|
||||
style={{marginTop: "0.5em"}}>{t('validerLicence', {count: selectedMembers.length})}
|
||||
</button>
|
||||
<ConfirmDialog title="Validation des licences"
|
||||
message={"Êtes-vous sûr de vouloir valider les " + selectedMembers.length + " licences ?"}
|
||||
<ConfirmDialog title={t('validationDesLicences')}
|
||||
message={t('validerLicence.msg', {count: selectedMembers.length})}
|
||||
onConfirm={handleValidation} id="confirm-validation"/>
|
||||
</>}
|
||||
|
||||
{source === "club" && <>
|
||||
<span>{selectedMembers.length} licences sélectionnée<br/>Total à régler : {selectedMembers.length * 15}€</span>
|
||||
<span>{t("payment.recap", {count: selectedMembers.length, total: selectedMembers.length * 15})}</span>
|
||||
<HaPay onClick={handlePay}/>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<FontAwesomeIcon icon={faCircleInfo} size="2xl" style={{color: "#74C0FC", marginRight: "0.25em"}}/>
|
||||
A propos de HelloAsso
|
||||
</div>
|
||||
<div className="card-body">
|
||||
Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à l’association choisie. Vous
|
||||
pouvez soutenir l’aide qu’ils apportent aux associations en laissant une contribution volontaire à HelloAsso au
|
||||
moment de votre paiement.
|
||||
{t('payment.info')}
|
||||
</div>
|
||||
<div
|
||||
className="card-body">{t('payment.ha.info')}</div>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -228,6 +191,7 @@ export function PayAndValidateList({source}) {
|
||||
}
|
||||
|
||||
function HaPay({onClick}) {
|
||||
const {t} = useTranslation();
|
||||
return <>
|
||||
<div className="HaPay" style={{marginTop: "0.5em"}}>
|
||||
<button className="HaPayButton" onClick={onClick}>
|
||||
@ -237,7 +201,7 @@ function HaPay({onClick}) {
|
||||
className="HaPayButtonLogo"
|
||||
/>
|
||||
<div className="HaPayButtonLabel">
|
||||
<span> Payer avec </span>
|
||||
<span> {t('payment.payerAvec')} </span>
|
||||
<svg
|
||||
width="73"
|
||||
height="14"
|
||||
@ -287,7 +251,7 @@ function HaPay({onClick}) {
|
||||
d="M3.875 3V4.5H7.625V3C7.625 1.96875 6.78125 1.125 5.75 1.125C4.69531 1.125 3.875 1.96875 3.875 3ZM2.75 4.5V3C2.75 1.35938 4.08594 0 5.75 0C7.39062 0 8.75 1.35938 8.75 3V4.5H9.5C10.3203 4.5 11 5.17969 11 6V10.5C11 11.3438 10.3203 12 9.5 12H2C1.15625 12 0.5 11.3438 0.5 10.5V6C0.5 5.17969 1.15625 4.5 2 4.5H2.75ZM1.625 6V10.5C1.625 10.7109 1.78906 10.875 2 10.875H9.5C9.6875 10.875 9.875 10.7109 9.875 10.5V6C9.875 5.8125 9.6875 5.625 9.5 5.625H2C1.78906 5.625 1.625 5.8125 1.625 6Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Paiement sécurisé</span>
|
||||
<span>{t('payment.paiementSécurisé')}</span>
|
||||
<img
|
||||
src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-visa.svg"
|
||||
alt="Logo Visa"
|
||||
@ -311,6 +275,7 @@ function HaPay({onClick}) {
|
||||
|
||||
function MakeCentralPanel({data, visibleMember, navigate, page, source, selectedMembers, setSelectedMembers}) {
|
||||
const lastCheckedRef = useRef(null);
|
||||
const {t} = useTranslation();
|
||||
|
||||
function handleCheckbox(e, memberId) {
|
||||
const isShiftKeyPressed = e.shiftKey;
|
||||
@ -364,8 +329,12 @@ function MakeCentralPanel({data, visibleMember, navigate, page, source, selected
|
||||
|
||||
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>
|
||||
<small>{t("page_info_full", {
|
||||
line: ((page - 1) * data.page_size) + 1,
|
||||
tt_line: (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size),
|
||||
page: page,
|
||||
tt_page: data.page_count,
|
||||
})}</small>
|
||||
<ul className="list-group">
|
||||
{visibleMember.map(member => (
|
||||
<MakeRow key={member.id} member={member} navigate={navigate} source={source} isChecked={selectedMembers.includes(member.id)}
|
||||
@ -387,6 +356,8 @@ function MakeCentralPanel({data, visibleMember, navigate, page, source, selected
|
||||
}
|
||||
|
||||
function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const rowContent = <>
|
||||
<div className="row">
|
||||
<div className="col-auto">
|
||||
@ -402,7 +373,7 @@ function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
|
||||
</div>
|
||||
{source === "club" ?
|
||||
<small>{member.categorie}</small>
|
||||
: <small>{member.club?.name || "Sans club"}</small>}
|
||||
: <small>{member.club?.name || t("club", {count: 0})}</small>}
|
||||
</>
|
||||
|
||||
if (member.licence != null) {
|
||||
@ -429,12 +400,13 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
|
||||
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]);
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div>
|
||||
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
||||
<option value="">--- toute les catégories ---</option>
|
||||
<option value="">{t('---TouteLesCatégories---')}</option>
|
||||
{Tools.CatList.map(cat => (
|
||||
<option key={cat} value={cat}>{getCatName(cat)}</option>
|
||||
))}
|
||||
@ -442,17 +414,17 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
|
||||
<option value={1}>Avec demande ou licence validée</option>
|
||||
<option value={2}>Demande en cours</option>
|
||||
<option value={5}>Demande complet</option>
|
||||
<option value={6}>Demande incomplet</option>
|
||||
<option value={1}>{t('membre.filtre.licences.1')}</option>
|
||||
<option value={2}>{t('membre.filtre.licences.2')}</option>
|
||||
<option value={5}>{t('membre.filtre.licences.5')}</option>
|
||||
<option value={6}>{t('membre.filtre.licences.6')}</option>
|
||||
</select>
|
||||
</div>
|
||||
{source !== "club" && <div className="mb-3">
|
||||
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}>
|
||||
<option value={2}>Tout les états de paiement</option>
|
||||
<option value={0}>Sans paiement</option>
|
||||
<option value={1}>Avec paiement</option>
|
||||
<option value={2}>{t('membre.filtre.payement.2')}</option>
|
||||
<option value={0}>{t('membre.filtre.payement.0')}</option>
|
||||
<option value={1}>{t('membre.filtre.payement.1')}</option>
|
||||
</select>
|
||||
</div>}
|
||||
</div>
|
||||
@ -461,13 +433,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
|
||||
function ClubSelectFilter({clubFilter, setClubFilter}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{data
|
||||
? <div className="mb-3">
|
||||
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
||||
<option value="">--- tout les clubs ---</option>
|
||||
<option value="null">--- sans club ---</option>
|
||||
<option value="">{t('---ToutLesClubs---')}</option>
|
||||
<option value="null">{t('---SansClub---')}</option>
|
||||
{data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -11,10 +11,12 @@ import {ClubPage} from "./club/ClubPage.jsx";
|
||||
import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx";
|
||||
import {StatsPage} from "./StatsPage.jsx";
|
||||
import {PayAndValidateList} from "../PayAndValidateList.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function AdminRoot() {
|
||||
const {t} = useTranslation();
|
||||
return <>
|
||||
<h1>Espace administration</h1>
|
||||
<h1>{t('espaceAdministration')}</h1>
|
||||
<LoadingProvider>
|
||||
<Outlet/>
|
||||
</LoadingProvider>
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import {Area, AreaChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Sector, Tooltip, XAxis, YAxis} from "recharts";
|
||||
import {useEffect, useState} from "react";
|
||||
import {getSaison} from "../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export default function StatsLazy({data}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="container">
|
||||
<div className="row" style={{marginTop: "2em"}}>
|
||||
<h3>Nombre de licences</h3>
|
||||
<h3>{t('nombreDeLicences')}</h3>
|
||||
<div className="col-lg-8" style={{margin: "auto"}}>
|
||||
<div style={{width: '100%', aspectRatio: 1.5}}>
|
||||
<NbGraph raw_data={data}/>
|
||||
@ -13,7 +16,7 @@ export default function StatsLazy({data}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "3em"}}>
|
||||
<h3>Nombre de licences par catégorie pour {getSaison()}</h3>
|
||||
<h3>{t('nombreDeLicencesParCatégorie', {saison: getSaison()})}</h3>
|
||||
<div className="col-lg-8" style={{margin: "auto"}}>
|
||||
<div style={{width: '100%', aspectRatio: 1.5}}>
|
||||
<CatGraph raw_data={data}/>
|
||||
@ -25,6 +28,7 @@ export default function StatsLazy({data}) {
|
||||
|
||||
function NbGraph({raw_data}) {
|
||||
const [showData, setShowData] = useState([])
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const data = []
|
||||
@ -57,10 +61,10 @@ function NbGraph({raw_data}) {
|
||||
<XAxis dataKey="name"/>
|
||||
<YAxis/>
|
||||
<Tooltip/>
|
||||
<Area type="monotone" dataKey="na" name="Non définie" stackId="1" stroke={colors[0]} fill={colors[0]}/>
|
||||
<Area type="monotone" dataKey="h" name="Homme" stackId="1" stroke={colors[1]} fill={colors[1]}/>
|
||||
<Area type="monotone" dataKey="f" name="Femme" stackId="1" stroke={colors[2]} fill={colors[2]}/>
|
||||
<Area type="monotone" dataKey="not_valid" name="Non validée" stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5"
|
||||
<Area type="monotone" dataKey="na" name={t('nonDéfinie')} stackId="1" stroke={colors[0]} fill={colors[0]}/>
|
||||
<Area type="monotone" dataKey="h" name={t('homme')} stackId="1" stroke={colors[1]} fill={colors[1]}/>
|
||||
<Area type="monotone" dataKey="f" name={t('femme')} stackId="1" stroke={colors[2]} fill={colors[2]}/>
|
||||
<Area type="monotone" dataKey="not_valid" name={t('nonValidée')} stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5"
|
||||
fillOpacity="30%"/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@ -3,14 +3,16 @@ import {FallingLines} from "react-loader-spinner";
|
||||
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function StatsPage() {
|
||||
const StatsLazy = lazy(() => import('./StatsLazy.jsx'))
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/stats`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div>
|
||||
<h2>Statistiques</h2>
|
||||
<h2>{t("stats")}</h2>
|
||||
<Suspense fallback={<FallingLines/>}>
|
||||
{data
|
||||
? <>
|
||||
|
||||
@ -5,9 +5,11 @@ import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Checkbox} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function AffiliationReqList() {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/affiliation/request`, setLoading, 1)
|
||||
@ -22,9 +24,9 @@ export function AffiliationReqList() {
|
||||
});
|
||||
|
||||
return <>
|
||||
<h2>Demande d'affiliation</h2>
|
||||
<h2>{t("nav.aff_request")}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
|
||||
<div>
|
||||
@ -39,7 +41,7 @@ export function AffiliationReqList() {
|
||||
</div>
|
||||
<div className="col-lg-3">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar data={data} saisonFilter={saisonFilter} setSaisonFilter={setSaisonFilter}/>
|
||||
</div>
|
||||
@ -51,10 +53,11 @@ export function AffiliationReqList() {
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, visibleRequest, navigate}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="mb-4">
|
||||
<small>{visibleRequest.length} ligne(s) affichée(s) sur {data.length}</small>
|
||||
<small>{t("page_info_ligne", {show: visibleRequest.length, total: data.length})}</small>
|
||||
<div className="list-group">
|
||||
{visibleRequest.map(req => (<MakeRow key={req.id} request={req} navigate={navigate}/>))}
|
||||
</div>
|
||||
@ -75,6 +78,8 @@ function MakeRow({request, navigate}) {
|
||||
let allSaison = []
|
||||
|
||||
function FiltreBar({data, saisonFilter, setSaisonFilter}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
@ -85,7 +90,7 @@ function FiltreBar({data, saisonFilter, setSaisonFilter}) {
|
||||
return <div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={String(saisonFilter)} onChange={event => setSaisonFilter(Number(event.target.value))}>
|
||||
<option value="">--- tout les saisons ---</option>
|
||||
<option value="">{t('all_season')}</option>
|
||||
{allSaison && allSaison.map((value, index) => {
|
||||
return <option key={index} value={value}>{value}-{value+1}</option>
|
||||
})
|
||||
|
||||
@ -3,25 +3,27 @@ import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function AffiliationReqPage() {
|
||||
const {id} = useParams()
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/affiliation/request/${id}`, setLoading, 1)
|
||||
|
||||
return <>
|
||||
<h2>Demande d'affiliation</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/affiliation/request")}>
|
||||
« retour
|
||||
<h2>{t("nav.aff_request")}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
|
||||
{t('back')}
|
||||
</button>
|
||||
<div>
|
||||
{data
|
||||
@ -36,19 +38,11 @@ export function AffiliationReqPage() {
|
||||
|
||||
function Content({data, refresh}) {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleRm = (reason) => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/affiliation/request/${data.id}?reason=${encodeURIComponent(reason)}`),
|
||||
{
|
||||
pending: "Suppression de la demande d'affiliation en cours",
|
||||
success: "Demande d'affiliation supprimée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de la demande d'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/affiliation/request/${data.id}?reason=${encodeURIComponent(reason)}`), getToastMessage('aff.toast.del')
|
||||
).then(_ => {
|
||||
navigate("/admin/affiliation/request")
|
||||
})
|
||||
@ -83,7 +77,7 @@ function Content({data, refresh}) {
|
||||
|
||||
if (mode === '0') {
|
||||
if (event.target['mode0_licence' + i].value === "") {
|
||||
toast.error("Veuillez saisir un numéro de licence valide pour le membre " + (i + 1));
|
||||
toast.error(t('aff.submit.error1', {id: i + 1}));
|
||||
err++;
|
||||
continue;
|
||||
}
|
||||
@ -91,7 +85,7 @@ function Content({data, refresh}) {
|
||||
formData.append(`m${i + 1}_licence`, event.target['licence' + i].value);
|
||||
} else if (mode === '1') {
|
||||
if (event.target['similar' + i].value === -1) {
|
||||
toast.error("Veuillez choisir un membre similaire pour le membre " + (i + 1));
|
||||
toast.error(t('aff.submit.error2', {id: i + 1}));
|
||||
err++;
|
||||
continue;
|
||||
}
|
||||
@ -104,7 +98,7 @@ function Content({data, refresh}) {
|
||||
|
||||
if (event.target['flexRadioDefault' + i].value === '0') {
|
||||
if (event.target['new_email' + i].value === "") {
|
||||
toast.error("Veuillez saisir un email valide pour le membre " + (i + 1));
|
||||
toast.error(t('aff.submit.error3', {id: i + 1}));
|
||||
err++;
|
||||
continue;
|
||||
}
|
||||
@ -112,7 +106,7 @@ function Content({data, refresh}) {
|
||||
formData.append(`m${i + 1}_email`, event.target['new_email' + i].value);
|
||||
} else {
|
||||
if (event.target['old_email' + i].value === "") {
|
||||
toast.error("Veuillez saisir un email valide pour le membre " + (i + 1));
|
||||
toast.error(t('aff.submit.error3', {id: i + 1}));
|
||||
err++;
|
||||
continue;
|
||||
}
|
||||
@ -126,32 +120,13 @@ function Content({data, refresh}) {
|
||||
return;
|
||||
|
||||
if (event.nativeEvent.submitter.value === "save") {
|
||||
toast.promise(
|
||||
apiAxios.put(`/affiliation/request/save`, formData),
|
||||
{
|
||||
pending: "Enregistrement de la demande d'affiliation en cours",
|
||||
success: "Demande d'affiliation enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement de la demande d'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
toast.promise(apiAxios.put(`/affiliation/request/save`, formData), getToastMessage('aff.toast.save')
|
||||
).then(_ => {
|
||||
refresh(`/affiliation/request/${data.id}`)
|
||||
})
|
||||
} else if (event.nativeEvent.submitter.value === "accept") {
|
||||
toast.promise(
|
||||
apiAxios.put(`/affiliation/request/apply`, formData),
|
||||
{
|
||||
pending: "Acceptation de l'affiliation en cours",
|
||||
success: "Affiliation acceptée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'acceptation de l'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.put(`/affiliation/request/apply`, formData), getToastMessage('aff.toast.accept')
|
||||
).then(_ => {
|
||||
navigate("/admin/affiliation/request")
|
||||
})
|
||||
@ -162,23 +137,23 @@ function Content({data, refresh}) {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
<input name="id" value={data.id} readOnly hidden/>
|
||||
<div className="card-header">Demande d'affiliation</div>
|
||||
<div className="card-header">{t("nav.aff_request")}</div>
|
||||
<div className="card-body text-center">
|
||||
{data.club && <h5>Ce club a déjà été affilié (affiliation n°{data.club_no_aff})</h5>}
|
||||
<h4 id="saison">Saison {data.saison}-{data.saison + 1}</h4>
|
||||
{data.club && <h5>{t('aff.info1', {no: data.club_no_aff})}</h5>}
|
||||
<h4 id="saison">{t('saison')} {data.saison}-{data.saison + 1}</h4>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="input-group">
|
||||
<span className="input-group-text" id="name">Nom du club</span>
|
||||
<input type="text" className="form-control" placeholder="Nom du club" aria-label="name"
|
||||
<span className="input-group-text" id="name">{t('aff.nomDuClub')}</span>
|
||||
<input type="text" className="form-control" placeholder={t('aff.nomDuClub')} aria-label="name"
|
||||
name="name" aria-describedby="name" defaultValue={data.name} required/>
|
||||
</div>
|
||||
{data.club && <div className="form-text" id="name">Ancien nom: {data.club_name}</div>}
|
||||
{data.club && <div className="form-text" id="name">{t('aff.ancienNom', {name: data.club_name})}</div>}
|
||||
</div>
|
||||
|
||||
<TextField name="state_id" text="SIRET ou RNA" value={data.stateId} disabled={true}/>
|
||||
<TextField name="address" text="Adresse" value={data.address}/>
|
||||
<TextField name="contact" text="Contact administratif" value={data.contact}/>
|
||||
<TextField name="state_id" text={t('siretOuRna')} value={data.stateId} disabled={true}/>
|
||||
<TextField name="address" text={t('adresse')} value={data.address}/>
|
||||
<TextField name="contact" text={t('contactAdministratif')} value={data.contact}/>
|
||||
|
||||
<img
|
||||
src={`${vite_url}/api/affiliation/request/${data.id}/logo`}
|
||||
@ -186,15 +161,15 @@ function Content({data, refresh}) {
|
||||
className="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="logo">Blason</label>
|
||||
<label className="input-group-text" htmlFor="logo">{t('blason')}</label>
|
||||
<input type="file" className="form-control" id="logo" name="logo"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||
</div>
|
||||
<div className="form-text" id="logo">Laissez vide pour ne rien changer.</div>
|
||||
<div className="form-text" id="logo">{t('keepEmpty')}</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="status">Status</label>
|
||||
<label className="input-group-text" htmlFor="status">{t('status')}</label>
|
||||
<a href={`${vite_url}/api/affiliation/request/${data.id}/status`} target='_blank'>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
||||
onClick={_ => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
@ -202,7 +177,7 @@ function Content({data, refresh}) {
|
||||
</a>
|
||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
|
||||
</div>
|
||||
<div className="form-text" id="status">Laissez vide pour ne rien changer.</div>
|
||||
<div className="form-text" id="status">{t('keepEmpty')}</div>
|
||||
</div>
|
||||
|
||||
{data.members.map((member, index) => {
|
||||
@ -214,10 +189,11 @@ function Content({data, refresh}) {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" value="accept" className="btn btn-success">Accepter</button>
|
||||
<button type="submit" value="save" className="btn btn-primary">Enregistrer</button>
|
||||
<button className="btn btn-danger" value="rm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Refuser</button>
|
||||
<ConfirmReasonDialog title="Refuser la demande" message="Êtes-vous sûr de vouloir refuser cette demande ?"
|
||||
<button type="submit" value="accept" className="btn btn-success">{t('button.accepter')}</button>
|
||||
<button type="submit" value="save" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
<button className="btn btn-danger" value="rm" data-bs-toggle="modal"
|
||||
data-bs-target="#confirm-delete">{t('button.refuser')}</button>
|
||||
<ConfirmReasonDialog title={t('aff.refuserLaDemande')} message={t('aff.refuserLaDemande.detail')}
|
||||
onConfirm={handleRm}/>
|
||||
</div>
|
||||
</div>
|
||||
@ -228,26 +204,28 @@ function Content({data, refresh}) {
|
||||
|
||||
function ConfirmReasonDialog({onConfirm, id = "confirm-delete"}) {
|
||||
const [reason, setReason] = useState("")
|
||||
const {t} = useTranslation();
|
||||
|
||||
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">
|
||||
<h4 className="modal-title" id="myModalLabel">"Refuser la demande"</h4>
|
||||
<h4 className="modal-title" id="myModalLabel">{t('aff.refuserLaDemande')}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="row mb-3">
|
||||
<div className="col">
|
||||
<label htmlFor="reason" className="form-label">Raison du refus</label>
|
||||
<label htmlFor="reason" className="form-label">{t('aff.raisonDuRefus')}</label>
|
||||
<textarea className="form-control" id="reason" name="reason" rows="3"
|
||||
placeholder="Veuillez indiquer la raison du refus" value={reason}
|
||||
placeholder={t('aff.raisonDuRefus.msg')} value={reason}
|
||||
onChange={e => setReason(e.target.value)}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<span>Êtes-vous sûr de vouloir refuser cette demande ?</span>
|
||||
<span>{t('aff.refusConfirm')}</span>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal">Annuler</button>
|
||||
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={_ => onConfirm(reason)}>Confirmer</a>
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={_ => onConfirm(reason)}>{t('button.confirmer')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -259,6 +237,7 @@ function MemberPart({index, member}) {
|
||||
const [current, setCurrent] = useState(-1)
|
||||
const [email, setEmail] = useState("")
|
||||
const [emailChoice, setEmailChoice] = useState("0")
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 1)
|
||||
@ -271,10 +250,10 @@ function MemberPart({index, member}) {
|
||||
|
||||
return <div className="col mb-4">
|
||||
<div className="card">
|
||||
<div className="card-header">Membre n°{index + 1}</div>
|
||||
<div className="card-header">{t('aff.membreNo', {no: index + 1})}</div>
|
||||
<input name={"mode" + index} value={mode} readOnly hidden/>
|
||||
<div className="card-body">
|
||||
<RoleList name={"role" + index} text="Rôle" value={member.role}/>
|
||||
<RoleList name={"role" + index} text={t('role')} value={member.role}/>
|
||||
|
||||
<div className="btn-group row" role="group">
|
||||
<div className="mb-2">
|
||||
@ -282,7 +261,7 @@ function MemberPart({index, member}) {
|
||||
<div className="card-header">
|
||||
<input type="radio" className="btn-check" id={"btnradio1" + index} autoComplete="off"
|
||||
checked={mode === 0} onChange={() => setMode(0)}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + index}>Par n° de licence</label>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + index}>{t('aff.byNoLicence')}</label>
|
||||
</div>
|
||||
|
||||
<div className="card-body">
|
||||
@ -298,7 +277,7 @@ function MemberPart({index, member}) {
|
||||
<div className="card-header">
|
||||
<input type="radio" className="btn-check" id={"btnradio2" + index} autoComplete="off"
|
||||
checked={mode === 1} onChange={() => setMode(1)}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + index}>Par Membre similaire</label>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + index}>{t('aff.byMembreSim')}</label>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<input name={"similar" + index} value={current} readOnly hidden/>
|
||||
@ -315,11 +294,11 @@ function MemberPart({index, member}) {
|
||||
<div className="card-header">
|
||||
<input type="radio" className="btn-check" id={"btnradio3" + index} autoComplete="off"
|
||||
checked={mode === 2} onChange={() => setMode(2)}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio3" + index}>Par Nouveau membre</label>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio3" + index}>{t('aff.byNewMenbre')}</label>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<TextField name={"lname" + index} text="Nom" value={member.lname} disabled={mode !== 2}/>
|
||||
<TextField name={"fname" + index} text="Prénom" value={member.fname} disabled={mode !== 2}/>
|
||||
<TextField name={"lname" + index} text={t('nom')} value={member.lname} disabled={mode !== 2}/>
|
||||
<TextField name={"fname" + index} text={t('prenom')} value={member.fname} disabled={mode !== 2}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -327,13 +306,13 @@ function MemberPart({index, member}) {
|
||||
|
||||
<div className="input-group">
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="radio" value="0" aria-label="nouvel email"
|
||||
<input className="form-check-input mt-0" type="radio" value="0" aria-label={t('nouvelEmail')}
|
||||
name={"flexRadioDefault" + index} checked={emailChoice === '0'}
|
||||
onChange={e => setEmailChoice(e.target.value)}/>
|
||||
</div>
|
||||
<span className="input-group-text">Nouvel email</span>
|
||||
<input type="Email" className="form-control" aria-label="nouvel email" defaultValue={member.email}
|
||||
name={"new_email" + index}/>
|
||||
<span className="input-group-text">{t('nouvelEmail')}</span>
|
||||
<input type="Email" className="form-control" aria-label={t('nouvelEmail')} defaultValue={member.email}
|
||||
name={"new_email" + index} autoComplete="false"/>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<div className="input-group-text">
|
||||
@ -341,9 +320,9 @@ function MemberPart({index, member}) {
|
||||
name={"flexRadioDefault" + index} disabled={!email} checked={emailChoice === '1'}
|
||||
onChange={e => setEmailChoice(e.target.value)}/>
|
||||
</div>
|
||||
<span className="input-group-text">Conserver l'ancien email</span>
|
||||
<span className="input-group-text">{t('conserverLancienEmail')}</span>
|
||||
<input type="Email" className="form-control" aria-label="ancien email" disabled={true}
|
||||
name={"old_email" + index} value={email}/>
|
||||
name={"old_email" + index} value={email} autoComplete="false"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -355,6 +334,7 @@ function MemberLicence({member, mode, index, setEmail}) {
|
||||
const [licence, setLicence] = useState(member.licence)
|
||||
const {data, refresh} = useFetch(null, setLoading, 1)
|
||||
const ref = useRef(-1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (licence === -1 || licence.length < 1 || licence === ref.current)
|
||||
@ -379,19 +359,21 @@ function MemberLicence({member, mode, index, setEmail}) {
|
||||
<input name={"mode0_licence" + index} value={data ? data.licence : ""} readOnly hidden/>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id={name}>Licence</span>
|
||||
<span className="input-group-text" id={name}>{t('licence')}</span>
|
||||
<input type="text" className="form-control" placeholder="00000" name={name}
|
||||
value={licence >= 0 ? String(licence) : ""} disabled={mode !== 0} required={mode === 0}
|
||||
onChange={event => setLicence(event.target.value)}/>
|
||||
</div>
|
||||
</div>
|
||||
{data && <span className="form-text">Nom: {data.lname} {data.fname}, Club: {data.club ? data.club.name : "Sans club"}</span>}
|
||||
{data && <span
|
||||
className="form-text">{t('nom')}: {data.lname} {data.fname}, {t('club', {count: 1})}: {data.club ? data.club.name : t('club', {count: 0})}</span>}
|
||||
</>
|
||||
}
|
||||
|
||||
function MemberSimilar({member, current, setCurrent, mode, index, setEmail}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data} = useFetch(`/member/find/similar?fname=${encodeURI(member.fname)}&lname=${encodeURI(member.lname)}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data && current >= 0 && mode === 1) {
|
||||
@ -410,7 +392,7 @@ function MemberSimilar({member, current, setCurrent, mode, index, setEmail}) {
|
||||
className={"list-group-item list-group-item-action" + (current === index ? " active" : "")}
|
||||
onClick={() => setCurrent(index)} disabled={mode !== 1}>
|
||||
{m.lname} {m.fname}<br/>
|
||||
<small>{m.club ? m.club.name : "Sans club"}</small>
|
||||
<small>{m.club ? m.club.name : t('club', {count: 0})}</small>
|
||||
</button>
|
||||
})}
|
||||
</div>
|
||||
|
||||
@ -4,16 +4,18 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEye, faFilePdf, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function AffiliationCard({clubData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [modalAffiliation, setModal] = useState({id: 0, club: clubData.id})
|
||||
const [affiliations, dispatch] = useReducer(SimpleReducer, [])
|
||||
@ -29,10 +31,10 @@ export function AffiliationCard({clubData}) {
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Affiliation</div>
|
||||
<div className="col">{t('affiliation')}</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#AffiliationModal"
|
||||
onClick={_ => setModal({id: 0, club: clubData.id, validate: 1})}>Ajouter
|
||||
onClick={_ => setModal({id: 0, club: clubData.id, validate: 1})}>{t('button.ajouter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -54,8 +56,8 @@ export function AffiliationCard({clubData}) {
|
||||
|
||||
<a href={`${vite_url}/api/club/${clubData.id}/affiliation`} target='#'>
|
||||
<button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}}
|
||||
onClick={e => null}>
|
||||
Téléchargée l'attestation d'affiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
onClick={() => null}>
|
||||
{t('dlAff')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
@ -76,16 +78,7 @@ function sendAffiliation(event, dispatch) {
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
toast.promise(
|
||||
apiAxios.post(`/affiliation/${formData.get('club')}?saison=${formData.get('saison')}`),
|
||||
{
|
||||
pending: "Enregistrement de l'affiliation en cours",
|
||||
success: "Affiliation enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement de l'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/affiliation/${formData.get('club')}?saison=${formData.get('saison')}`), getToastMessage('aff.toast.save2')
|
||||
).then(data => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||
dispatch({type: 'SORT', payload: (a, b) => b.saison - a.saison})
|
||||
@ -96,16 +89,7 @@ function sendAffiliation(event, dispatch) {
|
||||
function removeAffiliation(id, dispatch) {
|
||||
if (id <= 0) return
|
||||
toast.promise(
|
||||
apiAxios.delete(`/affiliation/${id}`),
|
||||
{
|
||||
pending: "Suppression de l'affiliation en cours",
|
||||
success: "Affiliation supprimée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de l'affiliation")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/affiliation/${id}`), getToastMessage('aff.toast.del2')
|
||||
).then(_ => {
|
||||
dispatch({type: 'REMOVE', payload: id})
|
||||
})
|
||||
@ -114,6 +98,7 @@ function removeAffiliation(id, dispatch) {
|
||||
function ModalContent({affiliation, dispatch}) {
|
||||
const navigate = useNavigate();
|
||||
const [saison, setSaison] = useState(0)
|
||||
const {t} = useTranslation();
|
||||
const setSeason = (event) => {
|
||||
setSaison(Number(event.target.value))
|
||||
}
|
||||
@ -130,14 +115,14 @@ function ModalContent({affiliation, dispatch}) {
|
||||
<input name="id" value={affiliation.id} readOnly hidden/>
|
||||
<input name="club" value={affiliation.club} readOnly hidden/>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Edition de l'affiliation</h1>
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">{t("editionDeL'affiliation")}</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">
|
||||
{affiliation.id === 0
|
||||
? <input type="number" className="form-control" placeholder="Saison" name="saison"
|
||||
? <input type="number" className="form-control" placeholder={t('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/></>}
|
||||
@ -145,10 +130,10 @@ function ModalContent({affiliation, dispatch}) {
|
||||
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
|
||||
</div>
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<span className="input-group-text" id="basic-addon2">État de la demande</span>
|
||||
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">Validée</span> :
|
||||
<span className="input-group-text" id="basic-addon2">{t('étatDeLaDemande')}</span>
|
||||
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">{t('validée')}</span> :
|
||||
<>
|
||||
<span className="input-group-text" id="basic-addon2">En attente</span>
|
||||
<span className="input-group-text" id="basic-addon2">{t('enAttente')}</span>
|
||||
<button type="button" className="btn btn-primary"
|
||||
onClick={() => navigate('/admin/affiliation/request/' + (affiliation.id * -1))}
|
||||
data-bs-dismiss="modal"><FontAwesomeIcon icon={faEye}/></button>
|
||||
@ -156,10 +141,10 @@ function ModalContent({affiliation, dispatch}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{affiliation.id === 0 && <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>}
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
{affiliation.id === 0 && <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.enregistrer')}</button>}
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
|
||||
{affiliation.id <= 0 || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeAffiliation(affiliation.id, dispatch)}>Supprimer</button>}
|
||||
onClick={() => removeAffiliation(affiliation.id, dispatch)}>{t('button.supprimer')}</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@ -3,14 +3,16 @@ import {useEffect, useState} from "react";
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {Checkbox} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {SearchBar} from "../../../components/SearchBar.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ClubList() {
|
||||
const {hash} = useLocation();
|
||||
const {t} = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
let page = Number(hash.substring(1));
|
||||
page = (page > 0) ? page : 1;
|
||||
@ -51,16 +53,7 @@ export function ClubList() {
|
||||
return;
|
||||
|
||||
toast.promise(
|
||||
apiAxios.get(`/affiliation/current`),
|
||||
{
|
||||
pending: "Chargement des affiliation...",
|
||||
success: "Affiliation chargées",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Impossible de charger les affiliations")
|
||||
}
|
||||
}
|
||||
})
|
||||
apiAxios.get(`/affiliation/current`), getToastMessage("club.toast.aff"))
|
||||
.then(data => {
|
||||
setAffiliationData(data.data);
|
||||
});
|
||||
@ -74,7 +67,7 @@ export function ClubList() {
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Club </h2>
|
||||
<h2>{t('club', {count: 1})}</h2>
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-lg-9">
|
||||
@ -90,13 +83,13 @@ export function ClubList() {
|
||||
<div className="col-lg-3">
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<button className="btn btn-primary" onClick={() => navigate("../affiliation/request")}>Demande d'affiliation en cours
|
||||
<button className="btn btn-primary" onClick={() => navigate("../affiliation/request")}>{t('demandeDaffiliationEnCours')}
|
||||
</button>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un club</button>
|
||||
<button className="btn btn-primary" onClick={() => navigate("new")}>{t('ajouterUnClub')}</button>
|
||||
</div>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar showAffiliationState={showAffiliationState} setShowAffiliationState={setShowAffiliationState} data={data}
|
||||
countryFilter={countryFilter} setCountryFilter={setCountryFilter}/>
|
||||
@ -109,6 +102,8 @@ export function ClubList() {
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, page}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const pages = []
|
||||
for (let i = 1; i <= data.page_count; i++) {
|
||||
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
|
||||
@ -118,8 +113,12 @@ function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, pa
|
||||
|
||||
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>
|
||||
<small>{t("page_info_full", {
|
||||
line: ((page - 1) * data.page_size) + 1,
|
||||
tt_line: (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size),
|
||||
page: page,
|
||||
tt_page: data.page_count,
|
||||
})}</small>
|
||||
<div className="list-group">
|
||||
{visibleclub.map(club => (<MakeRow key={club.id} club={club} navigate={navigate} showAffiliationState={showAffiliationState}/>))}
|
||||
</div>
|
||||
@ -165,6 +164,8 @@ function MakeRow({club, showAffiliationState, navigate}) {
|
||||
let allCountry = []
|
||||
|
||||
function FiltreBar({showAffiliationState, setShowAffiliationState, data, countryFilter, setCountryFilter}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
@ -175,11 +176,11 @@ function FiltreBar({showAffiliationState, setShowAffiliationState, data, country
|
||||
return <div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showAffiliationState} onChange={setShowAffiliationState}
|
||||
label="Afficher l'état des affiliation"/>
|
||||
label={t('afficherLétatDesAffiliation')}/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={countryFilter} onChange={event => setCountryFilter(event.target.value)}>
|
||||
<option value="">--- tout les pays ---</option>
|
||||
<option value="">{t('---ToutLesPays---')}</option>
|
||||
{allCountry && allCountry.map((value, index) => {
|
||||
return <option key={index} value={value}>{value}</option>
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import {useNavigate, useParams} from "react-router-dom";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {AffiliationCard} from "./AffiliationCard.jsx";
|
||||
@ -15,37 +15,30 @@ import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function ClubPage() {
|
||||
const {id} = useParams()
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/club/${id}`, setLoading, 1)
|
||||
const {data, error} = useFetch(`/club/${id}`, setLoading, 1)
|
||||
|
||||
const handleRm = () => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/club/${id}`),
|
||||
{
|
||||
pending: "Suppression du club en cours...",
|
||||
success: "Club supprimé avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression du club")
|
||||
}
|
||||
},
|
||||
}
|
||||
apiAxios.delete(`/club/${id}`), getToastMessage("club.toast.del")
|
||||
).then(_ => {
|
||||
navigate("/admin/club")
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Page club</h2>
|
||||
<h2>{t('pageClub')}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
{data
|
||||
? <div>
|
||||
@ -60,11 +53,11 @@ export function ClubPage() {
|
||||
<LoadingProvider><BureauCard clubData={data}/></LoadingProvider>
|
||||
<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 club
|
||||
data-bs-target="#confirm-delete">{t('supprimerLeClub')}
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmDialog title="Supprimer le club"
|
||||
message="Êtes-vous sûr de vouloir supprimer ce club ?"
|
||||
<ConfirmDialog title={t('supprimerLeClub')}
|
||||
message={t('supprimerLeClub.msg')}
|
||||
onConfirm={handleRm}/>
|
||||
|
||||
</div>
|
||||
@ -79,24 +72,14 @@ function InformationForm({data}) {
|
||||
const [switchOn, setSwitchOn] = useState(data.international);
|
||||
const [modal, setModal] = useState({id: -1})
|
||||
const locationModalCallback = useRef(null)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
toast.promise(
|
||||
apiAxios.put(`/club/${data.id}`, formData),
|
||||
{
|
||||
pending: "Enregistrement du club en cours",
|
||||
success: "Club enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement du club")
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
toast.promise(apiAxios.put(`/club/${data.id}`, formData), getToastMessage("club.toast.save"))
|
||||
}
|
||||
|
||||
return <>
|
||||
@ -104,11 +87,11 @@ function InformationForm({data}) {
|
||||
<div className="card mb-4">
|
||||
<input name="id" value={data.id} readOnly hidden/>
|
||||
<input name="clubId" value={data.clubId} readOnly hidden/>
|
||||
<div className="card-header">Affiliation n°{data.no_affiliation}</div>
|
||||
<div className="card-header">{t('affiliationNo', {no: data.no_affiliation})}</div>
|
||||
<div className="card-body text-center">
|
||||
|
||||
<TextField name="name" text="Nom" value={data.name}/>
|
||||
<CountryList name="country" text="Pays" value={data.country}/>
|
||||
<TextField name="name" text={t('nom')} value={data.name}/>
|
||||
<CountryList name="country" text={t('pays')} value={data.country}/>
|
||||
|
||||
<SmartLogoBackground
|
||||
src={`${vite_url}/api/club/${data.clubId}/logo`}
|
||||
@ -116,38 +99,38 @@ function InformationForm({data}) {
|
||||
imgClassName="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="logo">Blason</label>
|
||||
<label className="input-group-text" htmlFor="logo">{t('blason')}</label>
|
||||
<input type="file" className="form-control" id="logo" name="logo"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||
</div>
|
||||
<div className="form-text" id="logo">Laissez vide pour ne rien changer.</div>
|
||||
<div className="form-text" id="logo">{t('keepEmpty')}</div>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<div className="input-group-text">
|
||||
<input type="checkbox" className="form-check-input mt-0" name="international" id="international"
|
||||
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
|
||||
<label className="input-group-text" htmlFor="international">Club externe</label>
|
||||
<label className="input-group-text" htmlFor="international">{t('clubExterne')}</label>
|
||||
</div>
|
||||
</div>
|
||||
{!switchOn && <>
|
||||
<TextField name="state_id" text="SIRET ou RNA" value={data.state_id} required={false}/>
|
||||
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}
|
||||
<TextField name="state_id" text={t('siretOuRna')} value={data.state_id} required={false}/>
|
||||
<TextField name="contact_intern" text={t('contactInterne')} value={data.contact_intern} required={false}
|
||||
placeholder="example@test.com"/>
|
||||
<TextField name="address" text="Adresse administrative" value={data.address} required={false}
|
||||
placeholder="Adresse administrative"/>
|
||||
<TextField name="address" text={t('adresseAdministrative')} value={data.address} required={false}
|
||||
placeholder={t('adresseAdministrative')}/>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="status">Statue</label>
|
||||
<label className="input-group-text" htmlFor="status">{t('statue')}</label>
|
||||
<a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
||||
onClick={e => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
onClick={() => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
</button>
|
||||
</a>
|
||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
|
||||
</div>
|
||||
<div className="form-text" id="status">Laissez vide pour ne rien changer.</div>
|
||||
<div className="form-text" id="status">{t('keepEmpty')}</div>
|
||||
</div>
|
||||
|
||||
<ContactEditor data={data}/>
|
||||
@ -158,7 +141,7 @@ function InformationForm({data}) {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -173,10 +156,11 @@ export function BureauCard({clubData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1)
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Bureau</div>
|
||||
<div className="card-header">{t('bureau')}</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{data && data.map((d, index) => {
|
||||
|
||||
@ -2,21 +2,23 @@ import {useNavigate} from "react-router-dom";
|
||||
import {LoadingProvider} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
|
||||
import {useRef, useState} from "react";
|
||||
import {LocationEditor, LocationEditorModal} from "../../../components/Club/LocationEditor.jsx";
|
||||
import {ContactEditor} from "../../../components/Club/ContactEditor.jsx";
|
||||
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function NewClubPage() {
|
||||
const navigate = useNavigate()
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<h2>Page nouveau club</h2>
|
||||
<h2>{t('pageNouveauClub')}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
@ -35,6 +37,7 @@ function InformationForm() {
|
||||
const [modal, setModal] = useState({id: -1})
|
||||
const locationModalCallback = useRef(null)
|
||||
const navigate = useNavigate()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {data} = useFetch(`/club/contact_type`)
|
||||
|
||||
@ -44,16 +47,7 @@ function InformationForm() {
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
toast.promise(
|
||||
apiAxios.put(`/club`, formData),
|
||||
{
|
||||
pending: "Création du club en cours",
|
||||
success: "Club créé avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la création du club")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.put(`/club`, formData), getToastMessage("club.toast.new")
|
||||
).then(data => {
|
||||
navigate(`/admin/club/${data.data}`);
|
||||
})
|
||||
@ -62,15 +56,15 @@ function InformationForm() {
|
||||
return <>
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Nouveau club</div>
|
||||
<div className="card-header">{t('nouveauClub')}</div>
|
||||
<div className="card-body text-center">
|
||||
|
||||
<TextField name="name" text="Nom*"/>
|
||||
<CountryList name="country" text="Pays*" value={"FR"}/>
|
||||
<TextField name="name" text={t('nom')+"*"}/>
|
||||
<CountryList name="country" text={t('pays')+"*"} value={"FR"}/>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="logo">Blason</label>
|
||||
<label className="input-group-text" htmlFor="logo">{t('blason')}</label>
|
||||
<input type="file" className="form-control" id="logo" name="logo"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||
</div>
|
||||
@ -80,17 +74,17 @@ function InformationForm() {
|
||||
<div className="input-group-text">
|
||||
<input type="checkbox" className="form-check-input mt-0" name="international" id="international"
|
||||
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
|
||||
<label className="input-group-text" htmlFor="international">Club externe</label>
|
||||
<label className="input-group-text" htmlFor="international">{t('clubExterne')}</label>
|
||||
</div>
|
||||
</div>
|
||||
{!switchOn && <>
|
||||
<TextField name="state_id" text="SIRET ou RNA" required={false}/>
|
||||
<TextField name="contact_intern" text="Contact interne" required={false} placeholder="example@test.com"/>
|
||||
<TextField name="address" text="Adresse administrative" required={false} placeholder="Adresse administrative"/>
|
||||
<TextField name="state_id" text={t('siretOuRna')} required={false}/>
|
||||
<TextField name="contact_intern" text={t('contactInterne')} required={false} placeholder="example@test.com"/>
|
||||
<TextField name="address" text={t('adresseAdministrative')} required={false} placeholder="Adresse administrative"/>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="status">Status</label>
|
||||
<label className="input-group-text" htmlFor="status">{t('status')}</label>
|
||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,7 +97,7 @@ function InformationForm() {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,53 +1,32 @@
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} 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";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function CompteInfo({userData}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const creatAccount = () => {
|
||||
toast.promise(
|
||||
apiAxios.put(`/compte/${userData.id}/init`),
|
||||
{
|
||||
pending: 'Création du compte en cours',
|
||||
success: 'Compte créé avec succès 🎉',
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la création du compte")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
toast.promise(apiAxios.put(`/compte/${userData.id}/init`), getToastMessage("membre.toast.compte"))
|
||||
}
|
||||
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: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la définition de l'identifient")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
toast.promise(apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`), getToastMessage("membre.toast.id"))
|
||||
}
|
||||
|
||||
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
|
||||
{t('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
|
||||
data-bs-target="#comptIdModal">{t('définirLidDuCompte')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@ -60,12 +39,12 @@ export function CompteInfo({userData}) {
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Ce membre ne dispose pas de compte...</div>
|
||||
<div>{t('membre.noAccount')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button>
|
||||
<button className="btn btn-primary" onClick={creatAccount}>{t('membre.initaccount')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -77,18 +56,18 @@ export function CompteInfo({userData}) {
|
||||
<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>
|
||||
<h1 className="modal-title fs-5" id="comptIdModalLabel">{t('membre.initaccount.text1')}</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>
|
||||
<h5>{t('membre.initaccount.text2')}</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>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.appliquer')}</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -100,23 +79,24 @@ export function CompteInfo({userData}) {
|
||||
function CompteInfoContent({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{data
|
||||
? <>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Identifiant: {data.login}</div>
|
||||
<div>{t('membre.identifiant')}: {data.login}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
||||
<div>{t('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>{t('membre.emailVérifié')}: <ColoredCircle boolean={data.emailVerified}/></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import imageCompression from "browser-image-compression";
|
||||
import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function addPhoto(event, formData, send) {
|
||||
const imageFile = event.target.url_photo.files[0];
|
||||
@ -28,17 +29,19 @@ export function addPhoto(event, formData, send) {
|
||||
|
||||
export function InformationForm({data}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
|
||||
let error = false;
|
||||
if (event.target.country?.value === "NA") {
|
||||
toast.error('Veuillez sélectionner un pays valide 😕');
|
||||
toast.error(t('membre.info.error1'));
|
||||
error = true;
|
||||
}
|
||||
if (event.target.club?.value === "Sélectionner...") {
|
||||
toast.error('Veuillez sélectionner un club valide 😕');
|
||||
if (event.target.club?.value === t('selectionner...')) {
|
||||
toast.error(t('membre.info.error2'));
|
||||
error = true;
|
||||
}
|
||||
|
||||
@ -61,17 +64,14 @@ export function InformationForm({data}) {
|
||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
toast.promise(
|
||||
apiAxios.put(`/member/${data.id}`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(_ => {
|
||||
toast.success('Profil mis à jours avec succès 🎉');
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error(errFormater(e,'Échec de la mise à jours du profil 😕'));
|
||||
}).finally(() => {
|
||||
}), getToastMessage("membre.toast.save")
|
||||
).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
@ -81,34 +81,33 @@ export function InformationForm({data}) {
|
||||
|
||||
return <form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
<div className="card-header">{t('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"
|
||||
<TextField name="lname" text={t('nom')} value={data.lname}/>
|
||||
<TextField name="fname" text={t('prenom')} value={data.fname}/>
|
||||
<TextField name="email" text={t('email')} value={data.email} placeholder="name@example.com"
|
||||
type="email" required={false}/>
|
||||
<OptionField name="genre" text="Genre" value={data.genre}
|
||||
<OptionField name="genre" text={t('genre')} value={data.genre}
|
||||
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<CountryList name="country" text="Pays" value={data.country}/>
|
||||
<CountryList name="country" text={t('pays')} value={data.country}/>
|
||||
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
||||
inti_category={data.categorie} required={false}/>
|
||||
<div className="row">
|
||||
<ClubSelect defaultValue={data?.club?.id} name="club" na={true}/>
|
||||
</div>
|
||||
<RoleList name="role" text="Rôle" value={data.role}/>
|
||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||
<RoleList name="role" text={t('role')} value={data.role}/>
|
||||
<OptionField name="grade_arbitrage" text={t('gradeDarbitrage')} 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>
|
||||
<label className="input-group-text" htmlFor="url_photo">{t('photos')} {t('(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>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,8 +4,9 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function licenceReducer(licences, action) {
|
||||
switch (action.type) {
|
||||
@ -37,6 +38,7 @@ function licenceReducer(licences, action) {
|
||||
export function LicenceCard({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [modalLicence, setModal] = useState({id: -1, membre: userData.id})
|
||||
const [licences, dispatch] = useReducer(licenceReducer, [])
|
||||
@ -52,10 +54,10 @@ export function LicenceCard({userData}) {
|
||||
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">{t('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
|
||||
onClick={_ => setModal({id: -1, membre: userData.id})}>{t('button.ajouter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,16 +97,7 @@ function sendLicence(event, dispatch) {
|
||||
formData.set('certificate', `${event.target.certificateBy?.value}¤${event.target.certificateDate?.value}`)
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/${formData.get('membre')}`, formData),
|
||||
{
|
||||
pending: "Enregistrement de la licence en cours",
|
||||
success: "Licence enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement de la licence")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/licence/${formData.get('membre')}`, formData), getToastMessage("membre.toast.licence.save")
|
||||
).then(data => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||
dispatch({type: 'SORT'})
|
||||
@ -114,16 +107,7 @@ function sendLicence(event, dispatch) {
|
||||
|
||||
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: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de la licence")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/licence/${id}`), getToastMessage("membre.toast.licence.del")
|
||||
).then(_ => {
|
||||
dispatch({type: 'REMOVE', payload: id})
|
||||
})
|
||||
@ -136,6 +120,7 @@ function ModalContent({licence, dispatch}) {
|
||||
const [validate, setValidate] = useState(false)
|
||||
const [pay, setPay] = useState(false)
|
||||
const [isNew, setNew] = useState(true)
|
||||
const {t} = useTranslation();
|
||||
const setSeason = (event) => {
|
||||
setSaison(Number(event.target.value))
|
||||
}
|
||||
@ -179,7 +164,7 @@ function ModalContent({licence, 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>
|
||||
<h1 className="modal-title fs-5" id="LicenceModalLabel">{t('editionDeLaLicence')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
@ -194,25 +179,25 @@ function ModalContent({licence, dispatch}) {
|
||||
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
|
||||
</div>
|
||||
|
||||
<span>Certificat médical</span>
|
||||
<span>{t('certificatMédical')}</span>
|
||||
<div className="input-group mb-3 ">
|
||||
<span className="input-group-text" id="basic-addon2">Fait par</span>
|
||||
<input type="text" className="form-control" placeholder="Fait par" name="certificateBy"
|
||||
aria-label="Fait par" aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
|
||||
<span className="input-group-text" id="basic-addon2">, le</span>
|
||||
<span className="input-group-text" id="basic-addon2">{t('faitPar')}</span>
|
||||
<input type="text" className="form-control" placeholder={t('faitPar')} name="certificateBy"
|
||||
aria-label={t('faitPar')} aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
|
||||
<span className="input-group-text" id="basic-addon2">, {t('le')}</span>
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate"
|
||||
aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/>
|
||||
</div>
|
||||
<RadioGroupeOnOff name="pay" text="Paiement de la licence" value={pay}
|
||||
<RadioGroupeOnOff name="pay" text={t('paiementDeLaLicence')} value={pay}
|
||||
onChange={handlePayChange}/>
|
||||
<RadioGroupeOnOff name="validate" text="Validation de la licence" value={validate}
|
||||
<RadioGroupeOnOff name="validate" text={t('validationDeLaLicence')} 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>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.enregistrer')}</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeLicence(licence.id, dispatch)}>Supprimer</button>}
|
||||
onClick={() => removeLicence(licence.id, dispatch)}>{t('button.supprimer')}</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@ -7,42 +7,35 @@ import {PremForm} from "./PremForm.jsx";
|
||||
import {InformationForm} from "./InformationForm.jsx";
|
||||
import {LicenceCard} from "./LicenceCard.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SelectCard} from "./SelectCard.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function MemberPage() {
|
||||
const {id} = useParams()
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
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: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression du compte")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/member/${id}`), getToastMessage("membre.toast.del")
|
||||
).then(_ => {
|
||||
navigate(-1)
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<h2>{t('pageMembre')}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate(-1)}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
{data
|
||||
? <div>
|
||||
@ -63,10 +56,10 @@ export function MemberPage() {
|
||||
</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 className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">{t('supprimerLeCompte')}
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
||||
<ConfirmDialog title={t('supprimerLeCompte')} message={t('supprimerLeCompte.msg')}
|
||||
onConfirm={handleRm}/>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,8 +70,10 @@ export function MemberPage() {
|
||||
}
|
||||
|
||||
function PhotoCard({data}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">{data.licence ? "Licence n°"+data.licence : "Pas de licence"}</div>
|
||||
<div className="card-header">{data.licence ? t('licenceNo', {no: data.licence}) : t('pasDeLicence')}</div>
|
||||
<div className="card-body text-center">
|
||||
<div className="input-group mb-3">
|
||||
<img
|
||||
@ -89,7 +84,7 @@ function PhotoCard({data}) {
|
||||
<a href={`${vite_url}/api/member/${data.id}/licence`} target='#'>
|
||||
<button className="btn btn-primary" type="button" id="button-addon1"
|
||||
onClick={e => null}>
|
||||
Téléchargée la licence <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
{t('téléchargéeLaLicence')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
import {addPhoto} from "./InformationForm.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function NewMemberPage() {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<h2>{t('pageMembre')}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
@ -25,6 +27,7 @@ export function NewMemberPage() {
|
||||
function Form() {
|
||||
const navigate = useNavigate();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
@ -44,54 +47,51 @@ function Form() {
|
||||
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
toast.promise(
|
||||
apiAxios.post(`/member`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
toast.success('Profile crée avec succès 🎉');
|
||||
}), getToastMessage("membre.toast.save")
|
||||
).then(data => {
|
||||
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-header">{t('nouveauMembre')}</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"
|
||||
<TextField name="lname" text={t('nom')}/>
|
||||
<TextField name="fname" text={t('prenom')}/>
|
||||
<TextField name="email" text={t('email')} placeholder="name@example.com"
|
||||
type="email" required={false}/>
|
||||
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<CountryList name="country" text="Pays" value={"FR"}/>
|
||||
<OptionField name="genre" text={t('genre')} values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<CountryList name="country" text={t('pays')} value={"FR"}/>
|
||||
<BirthDayField/>
|
||||
<div className="row">
|
||||
<ClubSelect name="club" na={true}/>
|
||||
</div>
|
||||
<RoleList name="role" text="Rôle" value={'MEMBRE'}/>
|
||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={'NA'}
|
||||
<RoleList name="role" text={t('role')} value={'MEMBRE'}/>
|
||||
<OptionField name="grade_arbitrage" text={t('gradeDarbitrage')} 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>
|
||||
<label className="input-group-text" htmlFor="url_photo">{t('photos')}
|
||||
{t('(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>
|
||||
<button type="submit" className="btn btn-primary">{t('button.créer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} 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";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function PremForm({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmitPerm = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
@ -17,17 +20,14 @@ export function PremForm({userData}) {
|
||||
formData.append("create_compet", event.target.create_compet?.checked);
|
||||
formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
|
||||
|
||||
toast.promise(
|
||||
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(() => {
|
||||
}), getToastMessage("membre.toast.perm")
|
||||
).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
@ -35,21 +35,21 @@ export function PremForm({userData}) {
|
||||
|
||||
return <form onSubmit={handleSubmitPerm}>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Permission</div>
|
||||
<div className="card-header">{t('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>{t('membre.noAccount')}</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>}
|
||||
{userData.userId && <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,15 +60,16 @@ export function PremForm({userData}) {
|
||||
function PremFormContent({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="col">
|
||||
<h5>FFSAF intra</h5>
|
||||
<h5>{t('perm.ffsafIntra')}</h5>
|
||||
{data
|
||||
? <>
|
||||
<CheckField name="federation_admin" text="Administrateur de la fédération"
|
||||
<CheckField name="federation_admin" text={t('perm.administrateurDeLaFédération')}
|
||||
value={data.includes("federation_admin")}/>
|
||||
<CheckField name="create_compet" text="Créer des compétion"
|
||||
<CheckField name="create_compet" text={t('perm.créerDesCompétion')}
|
||||
value={data.includes("create_compet")}/>
|
||||
</>
|
||||
: error && <AxiosError error={error}/>}
|
||||
|
||||
@ -4,8 +4,9 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, CatList, errFormater, getCatName, getSaison} from "../../../utils/Tools.js";
|
||||
import {apiAxios, CatList, errFormater, getCatName, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function selectionReducer(selections, action) {
|
||||
switch (action.type) {
|
||||
@ -37,6 +38,7 @@ function selectionReducer(selections, action) {
|
||||
export function SelectCard({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [modalSelection, setModal] = useState({id: -1, membre: userData.id})
|
||||
const [selections, dispatch] = useReducer(selectionReducer, [])
|
||||
@ -52,10 +54,10 @@ export function SelectCard({userData}) {
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Sélection en équipe de France</div>
|
||||
<div className="col">{t('sélectionEnéquipeDeFrance')}</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#SelectionModal"
|
||||
onClick={() => setModal({id: -1, membre: userData.id, categorie: userData.categorie})}>Ajouter
|
||||
onClick={() => setModal({id: -1, membre: userData.id, categorie: userData.categorie})}>{t('button.ajouter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -64,7 +66,7 @@ export function SelectCard({userData}) {
|
||||
<ul className="list-group">
|
||||
{selections.map((selection, index) => {
|
||||
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} {t('en')} {getCatName(selection?.categorie)}</div>
|
||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||
data-bs-target="#SelectionModal" onClick={_ => setModal(selection)}>
|
||||
<FontAwesomeIcon icon={faPen}/></button>
|
||||
@ -98,16 +100,7 @@ function sendSelection(event, dispatch) {
|
||||
membre: event.target.membre.value,
|
||||
saison: event.target.saison.value,
|
||||
categorie: event.target.categorie.value,
|
||||
}),
|
||||
{
|
||||
pending: "Enregistrement de la séléction en cours",
|
||||
success: "Séléction enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement de la séléction")
|
||||
}
|
||||
}
|
||||
}
|
||||
}), getToastMessage("membre.toast.select.save")
|
||||
).then(data => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||
dispatch({type: 'SORT'})
|
||||
@ -117,16 +110,7 @@ function sendSelection(event, dispatch) {
|
||||
|
||||
function removeSelection(id, dispatch) {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/selection/${id}`),
|
||||
{
|
||||
pending: "Suppression de la séléction en cours",
|
||||
success: "Séléction supprimée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de la séléction")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/selection/${id}`), getToastMessage("membre.toast.select.del")
|
||||
).then(_ => {
|
||||
dispatch({type: 'REMOVE', payload: id})
|
||||
})
|
||||
@ -136,6 +120,7 @@ function ModalContent({selection, dispatch}) {
|
||||
const [saison, setSaison] = useState(0)
|
||||
const [cat, setCat] = useState("")
|
||||
const [isNew, setNew] = useState(true)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setSeason = (event) => {
|
||||
setSaison(Number(event.target.value))
|
||||
@ -159,15 +144,15 @@ function ModalContent({selection, dispatch}) {
|
||||
<input name="id" value={selection.id} readOnly hidden/>
|
||||
<input name="membre" value={selection.membre} readOnly hidden/>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="SelectionModalLabel">Edition de la séléction</h1>
|
||||
<h1 className="modal-title fs-5" id="SelectionModalLabel">{t('editionDeLaSéléction')}</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}/>
|
||||
? <input type="number" className="form-control" placeholder={t('saison')} name="saison"
|
||||
aria-label={t('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>
|
||||
@ -175,9 +160,9 @@ function ModalContent({selection, dispatch}) {
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">Catégorie</label>
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">{t('catégorie')}</label>
|
||||
<select className="form-select" id="inputGroupSelect01" value={cat} onChange={handleCatChange} name="categorie" required>
|
||||
<option>Choisir...</option>
|
||||
<option>{t('choisir...')}</option>
|
||||
{CatList.map((cat) => {
|
||||
return (<option key={cat} value={cat}>{getCatName(cat)}</option>)
|
||||
})}
|
||||
@ -186,23 +171,10 @@ function ModalContent({selection, dispatch}) {
|
||||
|
||||
</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>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.enregistrer')}</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeSelection(selection.id, dispatch)}>Supprimer</button>}
|
||||
onClick={() => removeSelection(selection.id, dispatch)}>{t('button.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>;
|
||||
}
|
||||
|
||||
@ -8,9 +8,12 @@ import {MyClubPage} from "./club/MyClubPage.jsx";
|
||||
import {PayAndValidateList} from "../PayAndValidateList.jsx";
|
||||
import {PaymentError} from "./PaymentError.jsx";
|
||||
import {PaymentReturn} from "./PaymentReturn.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ClubRoot() {
|
||||
const {userinfo} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
let club = ""
|
||||
if (userinfo?.groups) {
|
||||
for (let group of userinfo.groups) {
|
||||
@ -23,7 +26,7 @@ export function ClubRoot() {
|
||||
|
||||
return <>
|
||||
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}>
|
||||
<h3 style={{marginLeft: '0.75em'}}>Club: {club}</h3></div>
|
||||
<h3 style={{marginLeft: '0.75em'}}>{t("club", {count: 1})}: {club}</h3></div>
|
||||
<LoadingProvider>
|
||||
<Outlet/>
|
||||
</LoadingProvider>
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function PaymentError() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const error = searchParams.get("error");
|
||||
|
||||
return <div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-12" style={{textAlign: "center"}}>
|
||||
<h1>Erreur de paiement😕</h1>
|
||||
<p>Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.</p>
|
||||
<p><strong>Message d'erreur :</strong> {error}</p>
|
||||
<h1>{t('erreurDePaiement')}</h1>
|
||||
<p>{t('erreurDePaiement.msg')}</p>
|
||||
<p><strong>{t('erreurDePaiement.detail')}</strong> {error}</p>
|
||||
|
||||
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>Retour à la liste de membres</button>
|
||||
<button className="btn btn-primary" onClick={() => navigate("/club/member")}
|
||||
style={{marginTop: "0.5rem"}}>{t('retouràLaListeDeMembres')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function PaymentReturn() {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-12" style={{textAlign: "center"}}>
|
||||
<h1>🎉Votre paiement a été traité avec succès.🎉</h1>
|
||||
<p>Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.</p>
|
||||
<h1>{t('paymentOk')}</h1>
|
||||
<p>{t('paymentOk.msg')}</p>
|
||||
|
||||
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>Retour à la liste de membres</button>
|
||||
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>{t('retouràLaListeDeMembres')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@ -5,22 +5,23 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEye, faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function AffiliationCard({clubData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
const [modalAffiliation, setModal] = useState({id: 0, club: clubData.id})
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Affiliation</div>
|
||||
<div className="col">{t('affiliation')}</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#RenewModal">Renouveler</button>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#RenewModal">{t('renouveler')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -42,7 +43,7 @@ export function AffiliationCard({clubData}) {
|
||||
<a href={`${vite_url}/api/club/me/affiliation`} target='#'>
|
||||
<button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}}
|
||||
onClick={_ => null}>
|
||||
Télécharger l’attestation d’affiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
{t('dlAff')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
@ -60,11 +61,12 @@ export function AffiliationCard({clubData}) {
|
||||
|
||||
function ModalContent({affiliation}) {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{affiliation && <div>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Etat de l'affiliation</h1>
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">{t("editionDeL'affiliation")}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
@ -75,10 +77,10 @@ function ModalContent({affiliation}) {
|
||||
<span className="input-group-text" id="basic-addon2">{(affiliation.saison || 0) + 1}</span>
|
||||
</div>
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<span className="input-group-text" id="basic-addon2">État de la demande</span>
|
||||
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">Validée</span> :
|
||||
<span className="input-group-text" id="basic-addon2">{t('étatDeLaDemande')}</span>
|
||||
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">{t('validée')}</span> :
|
||||
<>
|
||||
<span className="input-group-text" id="basic-addon2">En attente</span>
|
||||
<span className="input-group-text" id="basic-addon2">{t('enAttente')}</span>
|
||||
<button type="button" className="btn btn-primary"
|
||||
onClick={() => navigate('/affiliation#e' + (affiliation.id * -1))}
|
||||
data-bs-dismiss="modal"><FontAwesomeIcon icon={faEye}/></button>
|
||||
@ -86,7 +88,7 @@ function ModalContent({affiliation}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -96,10 +98,11 @@ function ModalContent({affiliation}) {
|
||||
export function BureauCard({clubData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Bureau</div>
|
||||
<div className="card-header">{t('bureau')}</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{data && data.map((d, index) => {
|
||||
@ -126,6 +129,7 @@ export function BureauCard({clubData}) {
|
||||
|
||||
function ModalContent2({clubData, data}) {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const sendAffiliationRequest = (event) => {
|
||||
event.preventDefault()
|
||||
@ -150,13 +154,12 @@ function ModalContent2({clubData, data}) {
|
||||
|
||||
return <form onSubmit={sendAffiliationRequest}>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Renouvellement de l'affiliation</h1>
|
||||
<h1 className="modal-title fs-5" id="AffiliationModalLabel">{t('renouvellementDeLaffiliation')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain,
|
||||
vous pourrez les renseigner à la prochaine étape)</p>
|
||||
<p>{t('club.aff_renew.msg')}</p>
|
||||
{data && data.map((d, index) => {
|
||||
return <div key={index} className="input-group mb-1">
|
||||
<div className="input-group-text">
|
||||
@ -170,8 +173,8 @@ function ModalContent2({clubData, data}) {
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Suivant</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.suivant')}</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {AffiliationCard, BureauCard} from "./AffiliationCard.jsx";
|
||||
import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
@ -13,15 +13,17 @@ import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function MyClubPage() {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/me`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<h3>Données administratives</h3>
|
||||
<h3>{t('donnéesAdministratives')}</h3>
|
||||
{data
|
||||
? <div>
|
||||
<div className="row">
|
||||
@ -46,37 +48,27 @@ export function MyClubPage() {
|
||||
function InformationForm({data}) {
|
||||
const [modal, setModal] = useState({id: -1})
|
||||
const locationModalCallback = useRef(null)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
toast.promise(
|
||||
apiAxios.put(`/club/me`, formData),
|
||||
{
|
||||
pending: "Enregistrement des modifications en cours",
|
||||
success: "Modifications enregistrées avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement des modifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
toast.promise(apiAxios.put(`/club/me`, formData), getToastMessage('club.toast.save'))
|
||||
}
|
||||
|
||||
return <>
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<input name="id" value={data.id} readOnly hidden/>
|
||||
<div className="card-header">Affiliation n°{data.no_affiliation}</div>
|
||||
<div className="card-header">{t('affiliationNo', {no: data.no_affiliation})}</div>
|
||||
<div className="card-body text-center">
|
||||
<TextField name="name" text="Nom" value={data.name} disabled={true}/>
|
||||
<CountryList name="country" text="Pays" value={data.country} disabled={true}/>
|
||||
<TextField name="name" text={t('nom')} value={data.name} disabled={true}/>
|
||||
<CountryList name="country" text={t('pays')} value={data.country} disabled={true}/>
|
||||
|
||||
{!data.international && <>
|
||||
<TextField name="state_id" text="SIRET ou RNA" value={data.state_id} disabled={true}/>
|
||||
<TextField name="state_id" text={t('siretOuRna')} value={data.state_id} disabled={true}/>
|
||||
</>}
|
||||
|
||||
<div className="row mb-3">
|
||||
@ -91,17 +83,17 @@ function InformationForm({data}) {
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon1"
|
||||
onClick={_ => null}>
|
||||
<FontAwesomeIcon icon={faFilePdf} size="5x"></FontAwesomeIcon><br/>
|
||||
Voir les statues
|
||||
{t('voirLesStatues')}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-text" id="status">Pour modifier les informations ci-dessus, merci de contacter la FFSAF par mail.</div>
|
||||
<div className="form-text" id="status">{t('club.change.status')}</div>
|
||||
</div>
|
||||
|
||||
{!data.international && <>
|
||||
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}
|
||||
<TextField name="contact_intern" text={t('contactInterne')} value={data.contact_intern} required={false}
|
||||
placeholder="example@test.com"/>
|
||||
<TextField name="address" text="Adresse administrative" value={data.address} placeholder="Adresse administrative"/>
|
||||
<TextField name="address" text={t('adresseAdministrative')} value={data.address} placeholder={t('adresseAdministrative')}/>
|
||||
|
||||
<ContactEditor data={data}/>
|
||||
<LocationEditor data={data} setModal={setModal} sendData={locationModalCallback}/>
|
||||
@ -111,7 +103,7 @@ function InformationForm({data}) {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
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";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function CompteInfo({userData}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Compte</div>
|
||||
<div className="card-header">{t('compte')}</div>
|
||||
<div className="card-body text-center">
|
||||
{userData.userId
|
||||
? <CompteInfoContent userData={userData}/>
|
||||
@ -16,8 +16,8 @@ export function CompteInfo({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>{t('membre.noAccount')}<br/>
|
||||
{t('membre.noAccount.clubMsg')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -30,23 +30,24 @@ export function CompteInfo({userData}) {
|
||||
function CompteInfoContent({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{data
|
||||
? <>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Identifiant: {data.login}</div>
|
||||
<div>{t('membre.identifiant')}: {data.login}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
||||
<div>{t('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>{t('membre.emailVérifié')}: <ColoredCircle boolean={data.emailVerified}/></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
// noinspection DuplicatedCode
|
||||
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {addPhoto} from "../../admin/member/InformationForm.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function InformationForm({data}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(1)
|
||||
@ -24,17 +27,14 @@ export function InformationForm({data}) {
|
||||
formData.append("role", event.target.role?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
toast.promise(
|
||||
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 + ': ' + e.response.data + ')');
|
||||
}).finally(() => {
|
||||
}), getToastMessage("membre.toast.save")
|
||||
).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
})
|
||||
@ -44,33 +44,30 @@ export function InformationForm({data}) {
|
||||
|
||||
return <form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
<div className="card-header">{t('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" ttip={<small className="form-text">L'email sert à la création de compte pour se connecter au site et doit être unique. <br/>
|
||||
Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+<caractères alphanumériques>@exemple.com'}.<br/>
|
||||
Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com</small>}/>
|
||||
<OptionField name="genre" text="Genre" value={data.genre}
|
||||
<TextField name="lname" text={t('nom')} value={data.lname}/>
|
||||
<TextField name="fname" text={t('prenom')} value={data.fname}/>
|
||||
<TextField name="email" text={t('email')} value={data.email} placeholder="name@example.com"
|
||||
type="email" ttip={<small className="form-text">{t('membre.info.emailInfo')}</small>}/>
|
||||
<OptionField name="genre" text={t('genre')} value={data.genre}
|
||||
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<CountryList name="country" text="Pays" value={data.country}/>
|
||||
<CountryList name="country" text={t('pays')} value={data.country}/>
|
||||
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
||||
inti_category={data.categorie}/>
|
||||
<RoleList name="role" text="Rôle" value={data.role}/>
|
||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||
<RoleList name="role" text={t('role')} value={data.role}/>
|
||||
<OptionField name="grade_arbitrage" text={t('gradeDarbitrage')} 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>
|
||||
<label className="input-group-text" htmlFor="url_photo">{t('photos')} {t('(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>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,11 +2,12 @@ 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 {faEuroSign, faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {ColoredText} from "../../../components/ColoredCircle.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function licenceReducer(licences, action) {
|
||||
switch (action.type) {
|
||||
@ -32,6 +33,7 @@ function licenceReducer(licences, action) {
|
||||
|
||||
export function LicenceCard({userData}) {
|
||||
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false}
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
|
||||
@ -49,11 +51,11 @@ export function LicenceCard({userData}) {
|
||||
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">{t('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
|
||||
disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -93,16 +95,7 @@ function sendLicence(event, dispatch) {
|
||||
formData.set('certificate', `${event.target.certificateBy?.value}¤${event.target.certificateDate?.value}`)
|
||||
|
||||
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: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la demande de licence")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.post(`/licence/club/${formData.get('membre')}`, formData), getToastMessage("membre.toast.licence.ask")
|
||||
).then(data => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||
dispatch({type: 'SORT'})
|
||||
@ -112,16 +105,7 @@ function sendLicence(event, dispatch) {
|
||||
|
||||
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: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de la demande de licence")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/licence/club/${id}`), getToastMessage("membre.toast.licence.ask.del")
|
||||
).then(_ => {
|
||||
dispatch({type: 'REMOVE', payload: id})
|
||||
})
|
||||
@ -131,6 +115,7 @@ function ModalContent({licence, dispatch}) {
|
||||
const [certificateBy, setCertificateBy] = useState("")
|
||||
const [certificateDate, setCertificateDate] = useState("")
|
||||
const [isNew, setNew] = useState(true)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleCertificateByChange = (event) => {
|
||||
setCertificateBy(event.target.value.replaceAll('¤', ''));
|
||||
@ -163,36 +148,36 @@ function ModalContent({licence, dispatch}) {
|
||||
<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>
|
||||
{isNew ? t('demandeDeLicence') : t('editionDeLaDemande')}
|
||||
({t('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">
|
||||
<span>Certificat médical</span>
|
||||
<span>{t('certificatMédical')}</span>
|
||||
<div className="input-group mb-3 ">
|
||||
<span className="input-group-text" id="basic-addon2">Fait par</span>
|
||||
<input type="text" className="form-control" placeholder="Fait par" name="certificateBy" disabled={licence.validate}
|
||||
aria-label="Fait par" aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
|
||||
<span className="input-group-text" id="basic-addon2">, le</span>
|
||||
<span className="input-group-text" id="basic-addon2">{t('faitPar')}</span>
|
||||
<input type="text" className="form-control" placeholder={t('faitPar')} name="certificateBy" disabled={licence.validate}
|
||||
aria-label={t('faitPar')} aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
|
||||
<span className="input-group-text" id="basic-addon2">, {t('le')}</span>
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate" disabled={licence.validate}
|
||||
aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<div>Paiement de la licence: <ColoredText boolean={licence.pay}/></div>
|
||||
<div>{t('paiementDeLaLicence')}: <ColoredText boolean={licence.pay}/></div>
|
||||
</div>
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<div>Validation de la licence: <ColoredText boolean={licence.validate}/></div>
|
||||
<div>{t('validationDeLaLicence')}: <ColoredText boolean={licence.validate}/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{currentSaison && !licence.validate &&
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>}
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.enregistrer')}</button>}
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
|
||||
{currentSaison && !licence.validate && licence.id !== -1 && !licence.pay &&
|
||||
<button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeLicence(licence.id, dispatch)}>Annuler</button>}
|
||||
onClick={() => removeLicence(licence.id, dispatch)}>{t('button.annuler')}</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@ -6,42 +6,35 @@ import {CompteInfo} from "./CompteInfo.jsx";
|
||||
import {InformationForm} from "./InformationForm.jsx";
|
||||
import {LicenceCard} from "./LicenceCard.jsx";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SelectCard} from "./SelectCard.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function MemberPage() {
|
||||
const {id} = useParams()
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
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: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression du compte")
|
||||
}
|
||||
}
|
||||
}
|
||||
apiAxios.delete(`/member/club/${id}`), getToastMessage("membre.toast.del")
|
||||
).then(_ => {
|
||||
navigate(-1)
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<h2>{t('pageMembre')}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate(-1)}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
{data
|
||||
? <div>
|
||||
@ -62,10 +55,10 @@ export function MemberPage() {
|
||||
</div>
|
||||
{data.licence == null &&
|
||||
<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 className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">{t('supprimerLeCompte')}
|
||||
</button>
|
||||
</div>}
|
||||
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
|
||||
<ConfirmDialog title={t('supprimerLeCompte')} message={t('supprimerLeCompte.msg')}
|
||||
onConfirm={handleRm}/>
|
||||
</div>
|
||||
</div>
|
||||
@ -76,8 +69,10 @@ export function MemberPage() {
|
||||
}
|
||||
|
||||
function PhotoCard({data}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">{data.licence ? "Licence n°" + data.licence : "Pas de licence"}</div>
|
||||
<div className="card-header">{data.licence ? t('licenceNo', {no: data.licence}) : t('pasDeLicence')}</div>
|
||||
<div className="card-body text-center">
|
||||
<div className="input-group mb-3">
|
||||
<img
|
||||
@ -88,7 +83,7 @@ function PhotoCard({data}) {
|
||||
<a href={`${vite_url}/api/member/${data.id}/licence`} target='#'>
|
||||
<button className="btn btn-primary" type="button" id="button-addon1"
|
||||
onClick={e => null}>
|
||||
Téléchargée la licence <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
{t('téléchargéeLaLicence')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {apiAxios} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {addPhoto} from "../../admin/member/InformationForm.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function NewMemberPage() {
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<h2>Page membre</h2>
|
||||
<h2>{t('pageMembre')}</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
<div>
|
||||
<div className="row">
|
||||
@ -24,6 +26,7 @@ export function NewMemberPage() {
|
||||
function Form() {
|
||||
const navigate = useNavigate();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
@ -40,48 +43,45 @@ function Form() {
|
||||
formData.append("email", event.target.email?.value);
|
||||
|
||||
const send = (formData_) => {
|
||||
toast.promise(
|
||||
apiAxios.post(`/member/club`, formData_, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(data => {
|
||||
toast.success('Profile crée avec succès 🎉');
|
||||
}), getToastMessage("membre.toast.save")
|
||||
).then(data => {
|
||||
navigate(`/club/member/${data.data}`)
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ': ' + e.response.data + ')');
|
||||
}).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-header">{t('nouveauMembre')}</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"
|
||||
<TextField name="lname" text={t('nom')}/>
|
||||
<TextField name="fname" text={t('prenom')}/>
|
||||
<TextField name="email" text={t('email')} placeholder="name@example.com"
|
||||
type="email"/>
|
||||
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<CountryList name="country" text="Pays" value={"FR"}/>
|
||||
<OptionField name="genre" text={t('genre')} values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<CountryList name="country" text={t('pays')} value={"FR"}/>
|
||||
<BirthDayField/>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||
(optionnelle)</label>
|
||||
<label className="input-group-text" htmlFor="url_photo">{t('photos')}
|
||||
{t('(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>
|
||||
<button type="submit" className="btn btn-primary">{t('button.créer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,22 +2,24 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {getCatName} from "../../../utils/Tools.js";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function SelectCard({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Sélection en équipe de France</div>
|
||||
<div className="col">{t('sélectionEnéquipeDeFrance')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{data && data.sort((a, b) => b.saison - a.saison).map((selection, index) => {
|
||||
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} {t('en')} {getCatName(selection?.categorie)}</div>
|
||||
</div>
|
||||
})}
|
||||
{error && <AxiosError error={error}/>}
|
||||
|
||||
@ -6,31 +6,24 @@ import {CheckField, OptionField, TextField} from "../../components/MemberCustomF
|
||||
import {ClubSelect} from "../../components/ClubSelect.jsx";
|
||||
import {ConfirmDialog} from "../../components/ConfirmDialog.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, errFormater} from "../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage} from "../../utils/Tools.js";
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
export function CompetitionEdit() {
|
||||
const {id} = useParams()
|
||||
const navigate = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/competition/${id}?light=false`, setLoading, 1)
|
||||
|
||||
const handleRm = () => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/competition/${id}`),
|
||||
{
|
||||
pending: "Suppression de la compétition en cours...",
|
||||
success: "Compétition supprimé avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de la compétition")
|
||||
}
|
||||
},
|
||||
}
|
||||
apiAxios.delete(`/competition/${id}`), getToastMessage("comp.toast.del")
|
||||
).then(_ => {
|
||||
navigate("/competition")
|
||||
})
|
||||
@ -42,7 +35,7 @@ export function CompetitionEdit() {
|
||||
|
||||
return <>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/competition")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
<div>
|
||||
{data
|
||||
@ -51,8 +44,8 @@ export function CompetitionEdit() {
|
||||
<Content data={data} refresh={refresh}/>
|
||||
|
||||
{data.id !== null && <button style={{marginBottom: "1.5em", width: "100%"}} className="btn btn-primary"
|
||||
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier
|
||||
les participants</button>}
|
||||
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>
|
||||
{t('comp.modifierLesParticipants')}</button>}
|
||||
|
||||
{data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") &&
|
||||
<ContentSAFCAAndInternal data2={data} type={data.system}/>}
|
||||
@ -60,11 +53,11 @@ export function CompetitionEdit() {
|
||||
{data.id !== null && <>
|
||||
<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 la compétition
|
||||
data-bs-target="#confirm-delete">{t('comp.supprimerLaCompétition')}
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmDialog title="Supprimer la compétition"
|
||||
message="Êtes-vous sûr de vouloir supprimer cette compétition est tout les resultat associer?"
|
||||
<ConfirmDialog title={t('comp.supprimerLaCompétition')}
|
||||
message={t('comp.supprimerLaCompétition.msg')}
|
||||
onConfirm={handleRm}/>
|
||||
</>}
|
||||
</div>
|
||||
@ -80,6 +73,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(getDataPath, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
const [state2, dispatch2] = useReducer(SimpleReducer, [])
|
||||
@ -113,18 +107,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
out['admin'] = state.map(d => d.data)
|
||||
out['table'] = state2.map(d => d.data)
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(setDataPath, out),
|
||||
{
|
||||
pending: "Enregistrement des paramètres en cours",
|
||||
success: "Paramètres enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement des paramètres")
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
toast.promise(apiAxios.post(setDataPath, out), getToastMessage("comp.toast.params"))
|
||||
}
|
||||
|
||||
return <>
|
||||
@ -132,26 +115,26 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input name="id" value={data2.id || ""} readOnly hidden/>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Configuration SAFCA</div>
|
||||
<div className="card-header">{t('configuration')}</div>
|
||||
<div className="card-body text-center">
|
||||
|
||||
<div className="input-group mb-1">
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox" aria-label="Afficher le blason du club sur les écrans"
|
||||
<input className="form-check-input mt-0" type="checkbox" aria-label={t('comp.aff.blason')}
|
||||
defaultChecked={data.show_blason} name="show_blason"/>
|
||||
</div>
|
||||
<span className="input-group-text">Afficher le blason du club sur les écrans</span>
|
||||
<span className="input-group-text">{t('comp.aff.blason')}</span>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox" aria-label="Afficher le pays du combattant sur les écrans"
|
||||
<input className="form-check-input mt-0" type="checkbox" aria-label={t('comp.aff.flag')}
|
||||
defaultChecked={data.show_flag} name="show_flag"/>
|
||||
</div>
|
||||
<span className="input-group-text">Afficher le pays du combattant sur les écrans</span>
|
||||
<span className="input-group-text">{t('comp.aff.flag')}</span>
|
||||
</div>
|
||||
|
||||
<span className="input-group-text">Administrateur</span>
|
||||
<span className="input-group-text">{t('administrateur')}</span>
|
||||
<ul className="list-group form-control">
|
||||
{state.map((d, index) => {
|
||||
return <div key={index} className="input-group">
|
||||
@ -180,7 +163,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
</ul>
|
||||
|
||||
<div className="mb-3"></div>
|
||||
<span className="input-group-text">Table</span>
|
||||
<span className="input-group-text">{t('secrétariatsDeLice')}</span>
|
||||
<ul className="list-group form-control">
|
||||
{state2.map((d, index) => {
|
||||
return <div key={index} className="input-group">
|
||||
@ -210,7 +193,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -222,6 +205,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
function Content({data}) {
|
||||
const navigate = useNavigate();
|
||||
const [registerMode, setRegisterMode] = useState(data.registerMode || "FREE");
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
@ -250,7 +234,7 @@ function Content({data}) {
|
||||
const url = event.target.helloassoUrl?.value
|
||||
|
||||
if (!url || !event.target.data3?.value) {
|
||||
toast.error("Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.")
|
||||
toast.error(t('comp.ha.error1'))
|
||||
err = true;
|
||||
} else {
|
||||
const regex = /\/associations\/([^/]+)\/evenements\/([^/]+)/;
|
||||
@ -260,29 +244,29 @@ function Content({data}) {
|
||||
out['data1'] = match[1]
|
||||
out['data2'] = match[2]
|
||||
} else {
|
||||
toast.error("L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.")
|
||||
toast.error(t('comp.ha.error2'))
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
|
||||
out['data4'] = event.target.data4?.value
|
||||
if (!out['data4']) {
|
||||
toast.error("Veuillez renseigner l'email de réception des inscriptions échouées.")
|
||||
toast.error(t('comp.ha.error3'))
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (out['date'] > out['toDate']) {
|
||||
toast.error("La date de fin doit être postérieure à la date de début.")
|
||||
toast.error(t('comp.error1'))
|
||||
err = true;
|
||||
}
|
||||
|
||||
if ((out['registerMode'] === "FREE" || out['registerMode'] === "CLUB_ADMIN") && (!out['startRegister'] || !out['endRegister'])) {
|
||||
toast.error("Veuillez renseigner les dates de début et de fin d'inscription.")
|
||||
toast.error(t('comp.error2'))
|
||||
err = true;
|
||||
}
|
||||
if ((out['registerMode'] === "FREE" || out['registerMode'] === "CLUB_ADMIN") && out['startRegister'] > out['endRegister']) {
|
||||
toast.error("La date de fin d'inscription doit être postérieure à la date de début d'inscription.")
|
||||
toast.error(t('comp.error3'))
|
||||
err = true;
|
||||
}
|
||||
|
||||
@ -291,16 +275,7 @@ function Content({data}) {
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/competition`, out),
|
||||
{
|
||||
pending: "Enregistrement de la competition en cours",
|
||||
success: "Competition enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement de la competition")
|
||||
}
|
||||
},
|
||||
}
|
||||
apiAxios.post(`/competition`, out), getToastMessage("comp.toast.save")
|
||||
).then(data => {
|
||||
console.log(data.data)
|
||||
if (data.data.id !== undefined)
|
||||
@ -311,7 +286,7 @@ function Content({data}) {
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
<input name="id" value={data.id || ""} readOnly hidden/>
|
||||
<div className="card-header">{data.id ? "Edition compétition" : "Création compétition"}</div>
|
||||
<div className="card-header">{data.id ? t('comp.editionCompétition') : t('comp.créationCompétition')}</div>
|
||||
<div className="card-body text-center">
|
||||
|
||||
<div className="accordion" id="accordionExample">
|
||||
@ -319,7 +294,7 @@ function Content({data}) {
|
||||
<h2 className="accordion-header">
|
||||
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne"
|
||||
aria-expanded="false" aria-controls="collapseOne">
|
||||
Informations techniques
|
||||
{t('comp.informationsTechniques')}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseOne" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
|
||||
@ -340,27 +315,27 @@ function Content({data}) {
|
||||
<h2 className="accordion-header">
|
||||
<button className="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo"
|
||||
aria-expanded="true" aria-controls="collapseTwo">
|
||||
Informations générales sur la compétition
|
||||
{t('comp.informationsGénéralesSurLaCompétition')}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTwo" className="accordion-collapse collapse show" data-bs-parent="#accordionExample">
|
||||
<div className="accordion-body">
|
||||
<TextField name="name" text="Nom*" value={data.name}/>
|
||||
<TextField name="name" text={t('nom') + "*"} value={data.name}/>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="date">Date*</span>
|
||||
<span className="input-group-text" id="date">Du</span>
|
||||
<span className="input-group-text" id="date">{t('date')}*</span>
|
||||
<span className="input-group-text" id="date">{t('du')}</span>
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
|
||||
name="date" aria-describedby="date" defaultValue={data.date ? data.date.split('T')[0] : ''} required/>
|
||||
<span className="input-group-text" id="date">Au</span>
|
||||
<span className="input-group-text" id="date">{t('au')}</span>
|
||||
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="toDate"
|
||||
name="toDate" aria-describedby="toDate" defaultValue={data.toDate ? data.toDate.split('T')[0] : ''}
|
||||
required/>
|
||||
</div>
|
||||
<TextField name="description" text="Description" value={data.description} required={false}/>
|
||||
<TextField name="adresse" text="Adresse" value={data.adresse} required={false}/>
|
||||
<CheckField name="publicVisible" text="Visible par le public (Apparaît dans la liste des compétitions)"
|
||||
<TextField name="description" text={t('description')} value={data.description} required={false}/>
|
||||
<TextField name="adresse" text={t('adresse')} value={data.adresse} required={false}/>
|
||||
<CheckField name="publicVisible" text={t('comp.text2')}
|
||||
value={data.publicVisible} row={true}/>
|
||||
<small>Si non coché, la compétition ne sera visible que par les personnes pouvant y inscrire des participants.</small>
|
||||
<small>{t('comp.text3')}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -369,7 +344,7 @@ function Content({data}) {
|
||||
<h2 className="accordion-header">
|
||||
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree"
|
||||
aria-expanded="false" aria-controls="collapseThree">
|
||||
Informations sur le mode d'inscription
|
||||
{t('comp.informationsSurLeModeDinscription')}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseThree" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
|
||||
@ -377,24 +352,24 @@ function Content({data}) {
|
||||
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text">Qui peut inscrire</label>
|
||||
<label className="input-group-text">{t('comp.quiPeutInscrire')}</label>
|
||||
<select className="form-select" value={registerMode} onChange={event => setRegisterMode(event.target.value)}>
|
||||
<option value="FREE">Tous les membres de la FFSAF</option>
|
||||
<option value="CLUB_ADMIN">Responsables et bureaux des associations</option>
|
||||
<option value="ADMIN">Uniquement les administrateurs de la compétition</option>
|
||||
<option value="HELLOASSO">Billetterie HelloAsso</option>
|
||||
<option value="FREE">{t('comp.tousLesMembresDeLaFfsaf')}</option>
|
||||
<option value="CLUB_ADMIN">{t('comp.responsablesEtBureauxDesAssociations')}</option>
|
||||
<option value="ADMIN">{t('comp.uniquementLesAdministrateursDeLaCompétition')}</option>
|
||||
<option value="HELLOASSO">{t('comp.billetterieHelloasso')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3"
|
||||
style={{display: registerMode === "FREE" || registerMode === "CLUB_ADMIN" ? "flex" : "none"}}>
|
||||
<span className="input-group-text" id="startRegister">Date d'inscription</span>
|
||||
<span className="input-group-text" id="startRegister">Du</span>
|
||||
<span className="input-group-text" id="startRegister">{t('comp.dateDinscription')}</span>
|
||||
<span className="input-group-text" id="startRegister">{t('du')}</span>
|
||||
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
|
||||
name="startRegister" aria-describedby="startRegister"
|
||||
defaultValue={data.startRegister ? data.startRegister.substring(0, 16) : ''}/>
|
||||
<span className="input-group-text" id="endRegister">Au</span>
|
||||
<span className="input-group-text" id="endRegister">{t('au')}</span>
|
||||
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="endRegister"
|
||||
name="endRegister" aria-describedby="endRegister"
|
||||
defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/>
|
||||
@ -402,41 +377,28 @@ function Content({data}) {
|
||||
|
||||
<div style={{display: registerMode === "HELLOASSO" ? "initial" : "none"}}>
|
||||
<span style={{textAlign: "left"}}>
|
||||
<div>Afin de permettre une bonne interconnexion avec HelloAsso, merci de suivre les instructions suivantes :</div>
|
||||
<div>{t('comp.ha.text1')}</div>
|
||||
<ul>
|
||||
<li><strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à
|
||||
chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour
|
||||
qu'il redirige vers "https://intra.ffsaf.fr/api/webhook/ha". Pour ce faire, depuis la page d'accueil de
|
||||
votre association sur HelloAsso, allez dans <strong>Mon compte</strong> >
|
||||
<strong>Paramètres</strong> >
|
||||
<strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong>
|
||||
dans le champ <strong>Mon URL de callback</strong> et enregister.
|
||||
<li><Trans i18nKey="comp.ha.text2"></Trans>
|
||||
<img src="/img/HA-help-4.png" alt="" className="img-fluid" style={{objectFit: "contain"}}/></li>
|
||||
<li><strong>Copier-coller le nom exacte des tarifs</strong> <em>-sépare par des point-virgules-</em> qui donneront
|
||||
lieux à une inscription automatique. Tous ces tarifs doivent impérativement demander le numéro de licence
|
||||
en champs obligatoire. Pour ce faire, lors de la configuration de votre billetterie à l'étape n°3,
|
||||
cliquer sur <strong>+ Ajouter une information</strong>, saisissez l'intituler exact suivant
|
||||
<strong> Numéro de licence</strong>, dans <strong>Type de réponse souhaitée</strong> rentrer Nombre,
|
||||
sélectionner les tarifs entrés plus précédemment et rendre l'information obligatoire.
|
||||
<li><Trans i18nKey="comp.ha.text3"></Trans>
|
||||
<img src="/img/HA-help-3.png" className="img-fluid" alt="..." style={{object_fit: 'contain'}}/>
|
||||
<img src="/img/HA-help-2.png" className="img-fluid" alt="..." style={{object_fit: 'contain'}}/></li>
|
||||
<li><strong>Copier-coller l'url de votre billetterie</strong> dans le champs si dessous. Il devrais avoir la forme suivante:
|
||||
<em> https://www.helloasso.com/associations/<nom-asso-sur-helloasso>/evenements/<nom-billetterie></em></li>
|
||||
<li><Trans i18nKey="comp.ha.text4"></Trans></li>
|
||||
</ul>
|
||||
</span>
|
||||
|
||||
<TextField name="helloassoUrl" text="Url de la billetterie HelloAsso" required={false}
|
||||
<TextField name="helloassoUrl" text={t('comp.ha.text5')} required={false}
|
||||
value={data.data1 && data.data2 && `https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}` || ""}
|
||||
placeholder="https://www.helloasso.com/associations/nom-asso-sur-helloasso/evenements/nom-billetterie"/>
|
||||
|
||||
<TextField name="data3" text="Tarifs HelloAsso"
|
||||
<TextField name="data3" text={t('comp.ha.tarifsHelloasso')}
|
||||
value={data.data3 || ""} required={false}
|
||||
placeholder="Tarif1;Tarif2;Tarif3"/>
|
||||
|
||||
<TextField name="data4" text="Email de réception des inscriptions échoué"
|
||||
<TextField name="data4" text={t('comp.ha.emailDeRéceptionDesInscriptionséchoué')}
|
||||
value={data.data4 || ""} required={false}/>
|
||||
<small>Si pour une raison quelconque l'inscription automatique échoue, un email sera envoyé à cette adresse pour
|
||||
vous en informer</small>
|
||||
<small>{t('comp.ha.text6')}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -447,7 +409,7 @@ function Content({data}) {
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,8 @@ import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useAuth} from "../../hooks/useAuth.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
|
||||
export function CompetitionList() {
|
||||
@ -28,14 +30,15 @@ export function CompetitionList() {
|
||||
|
||||
function MakeCentralPanel({data, navigate}) {
|
||||
const {userinfo} = useAuth()
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{userinfo?.roles?.includes("create_compet") &&
|
||||
<div className="col mb-2" style={{textAlign: 'right', marginTop: '1em'}}>
|
||||
<button type="button" className="btn btn-primary" onClick={() => navigate("/competition/0")}>Nouvelle compétition</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => navigate("/competition/0")}>{t('comp.nouvelleCompétition')}</button>
|
||||
</div>}
|
||||
<div className="mb-4">
|
||||
<h3>Compétition future</h3>
|
||||
<h3>{t('comp.compétitionFuture')}</h3>
|
||||
<div className="list-group">
|
||||
{data.filter(req => new Date(req.toDate.split('T')[0]) >= new Date()).sort((a, b) => {
|
||||
return new Date(a.date.split('T')[0]) - new Date(b.date.split(')T')[0])
|
||||
@ -43,7 +46,7 @@ function MakeCentralPanel({data, navigate}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3>Compétition passée</h3>
|
||||
<h3>{t('comp.compétitionPassée')}</h3>
|
||||
<div className="list-group">
|
||||
{data.filter(req => new Date(req.toDate.split('T')[0]) < new Date()).sort((a, b) => {
|
||||
return new Date(b.date.split('T')[0]) - new Date(a.date.split(')T')[0])
|
||||
@ -55,25 +58,26 @@ function MakeCentralPanel({data, navigate}) {
|
||||
|
||||
const inscText = (type) => {
|
||||
if (type === "FREE") {
|
||||
return "Inscriptions libres"
|
||||
return i18n.t('comp.inscriptionsLibres')
|
||||
} else if (type === "CLUB_ADMIN") {
|
||||
return "Inscriptions par les responsables de club"
|
||||
return i18n.t('comp.inscriptionsParLesResponsablesDeClub')
|
||||
} else if (type === "ADMIN") {
|
||||
return "Inscriptions par les administrateurs de la compétition"
|
||||
return i18n.t('comp.inscriptionsParLesAdministrateursDeLaCompétition')
|
||||
} else if (type === "HELLOASSO") {
|
||||
return "Inscriptions sur la billetterie HelloAsso"
|
||||
return i18n.t('comp.inscriptionsSurLaBilletterieHelloasso')
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function MakeRow({data, navigate}) {
|
||||
const {t} = useTranslation();
|
||||
return <div className="list-group-item list-group-item-action"
|
||||
onClick={() => data.canEdit ? navigate("" + data.id) : navigate("" + data.id + "/view")}>
|
||||
<div className="row justify-content-between align-items-start ">
|
||||
<div className="ms-2 col-auto">
|
||||
<div><strong>{data.name}</strong> <small>par {data.clubName}</small></div>
|
||||
<small>Du {new Date(data.date.split('T')[0]).toLocaleDateString()} au {new Date(data.toDate.split('T')[0]).toLocaleDateString()}</small>
|
||||
<div><strong>{data.name}</strong> <small>{t('par')} {data.clubName}</small></div>
|
||||
<small>{t('du')} {new Date(data.date.split('T')[0]).toLocaleDateString()} {t('au')} {new Date(data.toDate.split('T')[0]).toLocaleDateString()}</small>
|
||||
</div>
|
||||
<div className="ms-2 col-auto">
|
||||
<small style={{textAlign: 'right'}}>{inscText(data.registerMode)}</small>
|
||||
|
||||
@ -4,7 +4,7 @@ import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {apiAxios, CatList, getCatName} from "../../utils/Tools.js";
|
||||
import {apiAxios, CatList, getCatName, getToastMessage} from "../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
@ -12,6 +12,7 @@ import {faAdd, faGavel, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./CompetitionRegisterAdmin.css"
|
||||
import * as XLSX from "xlsx-js-style";
|
||||
import {useCountries} from "../../hooks/useCountries.jsx";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
export function CompetitionRegisterAdmin({source}) {
|
||||
const {id} = useParams()
|
||||
@ -20,6 +21,7 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
const [clubFilter, setClubFilter] = useState("")
|
||||
const [catFilter, setCatFilter] = useState("")
|
||||
const [modalState, setModalState] = useState({})
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/competition/${id}/register/${source}`, setLoading, 1)
|
||||
@ -38,13 +40,8 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
}, [data, clubFilter, catFilter]);
|
||||
|
||||
const sendRegister = (new_state) => {
|
||||
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
|
||||
pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: {
|
||||
render({data}) {
|
||||
return data.response.data || "Combattant non trouvé"
|
||||
}
|
||||
}
|
||||
}).then((response) => {
|
||||
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), getToastMessage("comp.toast.register.add")
|
||||
).then((response) => {
|
||||
if (response.data.error) {
|
||||
return
|
||||
}
|
||||
@ -55,10 +52,10 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h2>Combattants inscrits</h2>
|
||||
<h2>{t('comp.combattantsInscrits')}</h2>
|
||||
<button type="button" className="btn btn-link"
|
||||
onClick={() => source === "admin" ? navigate("/competition/" + id) : navigate("/competition/" + id + "/view")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
|
||||
<div className="row">
|
||||
@ -72,17 +69,17 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
<div className="col-lg-3">
|
||||
<div className="mb-1">
|
||||
<button type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registerModal"
|
||||
onClick={() => setModalState({id: 0})}>Ajouter un combattant
|
||||
onClick={() => setModalState({id: 0})}>{t('comp.ajouterUnCombattant')}
|
||||
</button>
|
||||
</div>
|
||||
{source === "admin" && <div className="mb-4">
|
||||
<button type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registerModal"
|
||||
onClick={() => setModalState({id: -793548328091516928})}>Ajouter un invité
|
||||
onClick={() => setModalState({id: -793548328091516928})}>{t('comp.ajouterUnInvité')}
|
||||
</button>
|
||||
</div>}
|
||||
<QuickAdd sendRegister={sendRegister} source={source}/>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} catFilter={catFilter}
|
||||
setCatFilter={setCatFilter} source={source}/>
|
||||
@ -97,6 +94,7 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
}
|
||||
|
||||
function QuickAdd({sendRegister, source}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleAdd = (licence) => {
|
||||
console.log("Quick add licence: " + licence)
|
||||
@ -107,10 +105,10 @@ function QuickAdd({sendRegister, source}) {
|
||||
}
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Ajout rapide</div>
|
||||
<div className="card-header">{t('comp.ajoutRapide')}</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<span>N° de licence</span>
|
||||
<span>{t('comp.noDeLicence')}</span>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<input type="text" className="form-control" placeholder="12345" id="quickAddLicence"
|
||||
@ -144,11 +142,12 @@ function SearchMember({sendRegister}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/members`, setLoading, 1)
|
||||
const [suggestions, setSuggestions] = useState([])
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleAdd = (name) => {
|
||||
const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name);
|
||||
if (!member) {
|
||||
toast.error("Combattant non trouvé");
|
||||
toast.error(t('comp.combattantNonTrouvé'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -173,7 +172,7 @@ function SearchMember({sendRegister}) {
|
||||
|
||||
return <>
|
||||
{data ? <div className="row mb-3" style={{marginTop: "0.5em"}}>
|
||||
<span>Prénom et nom</span>
|
||||
<span>{t('prénomEtNom')}</span>
|
||||
<AutoCompleteInput suggestions={suggestions} handleAdd={handleAdd}/>
|
||||
</div> : error ? <AxiosError error={error}/> : <Def/>}
|
||||
</>
|
||||
@ -185,6 +184,7 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [activeSuggestion, setActiveSuggestion] = useState(0);
|
||||
const wrapperRef = useRef(null);
|
||||
const {t} = useTranslation();
|
||||
|
||||
// Filtre les suggestions
|
||||
useEffect(() => {
|
||||
@ -252,7 +252,7 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => inputValue && setShowSuggestions(true)}
|
||||
placeholder="Rechercher..."
|
||||
placeholder={t('rechercher...')}
|
||||
aria-autocomplete="list"
|
||||
aria-expanded={showSuggestions}
|
||||
aria-controls="suggestions-list"
|
||||
@ -286,6 +286,7 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
|
||||
|
||||
function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
const country = useCountries('fr')
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [licence, setLicence] = useState("")
|
||||
const [fname, setFname] = useState("")
|
||||
@ -357,31 +358,31 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
}}>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5"
|
||||
id="registerLabel">{editMode ? "Modification d'" : "Ajouter "}un {modalState.id >= 0 ? "combattant" : "invité"}</h1>
|
||||
id="registerLabel">{editMode ? t('modification') : t('ajout')} {t('dun')} {modalState.id >= 0 ? t('combattant') : t('invité')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{modalState.id < 0 &&
|
||||
<div className="mb-2">Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via
|
||||
ce formulaire ne pourront pas voir leur résultat depuis leur profil.</div>}
|
||||
<div className="mb-2">{t('comp.modal.text1')}</div>}
|
||||
<div className="card" style={{marginBottom: "1em"}}>
|
||||
<div className="card-header">{modalState.id >= 0 ? "Recherche*" : "Information"}</div>
|
||||
<div className="card-header">{modalState.id >= 0 ? t('comp.modal.recherche') : t('comp.modal.information')}</div>
|
||||
<div className="card-body">
|
||||
<div className="row" hidden={modalState.id < 0}>
|
||||
<div className="col">
|
||||
<input type="number" min={0} step={1} className="form-control" placeholder="N° de licence" name="licence"
|
||||
<input type="number" min={0} step={1} className="form-control" placeholder={t("comp.noDeLicence")}
|
||||
name="licence"
|
||||
value={licence} onChange={e => setLicence(e.target.value)} disabled={editMode}/>
|
||||
</div>
|
||||
</div>
|
||||
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>Ou</h5>
|
||||
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>{t('ou')}</h5>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<input type="text" className="form-control" placeholder="Prénom" name="fname"
|
||||
<input type="text" className="form-control" placeholder={t('prenom')} name="fname"
|
||||
disabled={editMode && modalState.id >= 0}
|
||||
value={fname} onChange={e => setFname(e.target.value)}/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<input type="text" className="form-control" placeholder="Nom" name="lname"
|
||||
<input type="text" className="form-control" placeholder={t('nom')} name="lname"
|
||||
disabled={editMode && modalState.id >= 0}
|
||||
value={lname} onChange={e => setLname(e.target.value)}/>
|
||||
</div>
|
||||
@ -390,16 +391,16 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={modalState.id >= 0}>
|
||||
<span className="input-group-text" id="categorie">Club</span>
|
||||
<input type="text" className="form-control" placeholder="Club" name="club"
|
||||
<span className="input-group-text" id="categorie">{t("club", {count: 1})}</span>
|
||||
<input type="text" className="form-control" placeholder={t("club", {count: 1})} name="club"
|
||||
value={club} onChange={e => setClub(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={modalState.id >= 0}>
|
||||
<span className="input-group-text" id="categorie">Catégorie</span>
|
||||
<span className="input-group-text" id="categorie">{t('catégorie')}</span>
|
||||
<select id="inputState2" className="form-select" value={gcat}
|
||||
onChange={(e) => setGCat(e.target.value)}>
|
||||
<option>-- Sélectionner catégorie --</option>
|
||||
<option>{t('--SélectionnerCatégorie--')}</option>
|
||||
{CatList.map((cat, index) => {
|
||||
return (<option key={index} value={cat}>{getCatName(cat)}</option>)
|
||||
})}
|
||||
@ -407,7 +408,7 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={modalState.id >= 0}>
|
||||
<span className="input-group-text" id="categorie">Pays</span>
|
||||
<span className="input-group-text" id="pays">{t('pays')}</span>
|
||||
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
|
||||
{country && Object.keys(country).sort((a, b) => {
|
||||
if (a < b) return -1
|
||||
@ -420,41 +421,40 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={modalState.id >= 0}>
|
||||
<span className="input-group-text" id="categorie">Genre</span>
|
||||
<select className="form-select" aria-label="categorie" name="categorie" value={genre}
|
||||
<span className="input-group-text" id="genre">{t('genre')}</span>
|
||||
<select className="form-select" aria-label={t('categorie')} name="genre" value={genre}
|
||||
onChange={e => setGenre(e.target.value)}>
|
||||
<option value={"NA"}>NA</option>
|
||||
<option value={"H"}>H</option>
|
||||
<option value={"F"}>F</option>
|
||||
<option value={"H"}>{t('h')}</option>
|
||||
<option value={"F"}>{t('f')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="weight">Poids (en kg)</span>
|
||||
<span className="input-group-text" id="weight">{t('comp.modal.poids')}</span>
|
||||
<input type="number" min={1} step={1} className="form-control" placeholder="42" aria-label="weight"
|
||||
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={modalState.id < 0}>
|
||||
<span className="input-group-text" id="categorie">Surclassement</span>
|
||||
<select className="form-select" aria-label="categorie" name="categorie" value={cat}
|
||||
<span className="input-group-text" id="surclassement">{t('comp.modal.surclassement')}</span>
|
||||
<select className="form-select" aria-label={t('comp.modal.surclassement')} name="surclassement" value={cat}
|
||||
onChange={e => setCat(Number(e.target.value))}>
|
||||
<option value={0}>Aucun</option>
|
||||
<option value={1}>+1 catégorie</option>
|
||||
<option value={2}>+2 catégorie</option>
|
||||
<option value={0}>{t('aucun')}</option>
|
||||
<option value={1}>{t('1Catégorie')}</option>
|
||||
<option value={2}>{t('2Catégorie')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{editMode && source === "admin" && <div className="form-check form-switch form-check-reverse" hidden={modalState.id < 0}>
|
||||
<input className="form-check-input" type="checkbox" id="switchCheckReverse" checked={lockEdit}
|
||||
onChange={e => setLockEdit(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="switchCheckReverse">Empêcher les membres/club de modifier cette
|
||||
inscription</label>
|
||||
<label className="form-check-label" htmlFor="switchCheckReverse">{t('comp.modal.text2')}</label>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-primary">{editMode ? "Modifier" : "Ajouter"}</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">Annuler</button>
|
||||
<button type="submit" className="btn btn-primary">{editMode ? t('button.modifier') : t('button.ajouter')}</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">{t('button.annuler')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -466,6 +466,7 @@ let allClub = []
|
||||
let allCat = []
|
||||
|
||||
function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, source}) {
|
||||
const {t} = useTranslation();
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
allClub.push(...data.map((e) => e.club?.name))
|
||||
@ -477,7 +478,7 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
|
||||
return <div>
|
||||
{source === "admin" && <div className="mb-3">
|
||||
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
||||
<option value="">--- tout les clubs ---</option>
|
||||
<option value="">{t('---ToutLesClubs---')}</option>
|
||||
{allClub && allClub.map((value, index) => {
|
||||
return <option key={index} value={value}>{value}</option>
|
||||
})}
|
||||
@ -485,7 +486,7 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
|
||||
</div>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
||||
<option value="">--- toute les catégories ---</option>
|
||||
<option value="">{t('---TouteLesCatégories---')}</option>
|
||||
{allCat && allCat.map((value, index) => {
|
||||
return <option key={index} value={value}>{value}</option>
|
||||
})}
|
||||
@ -497,12 +498,11 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
|
||||
function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const registerType = searchParams.get("type") || "FREE";
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
{(registerType === "FREE" || registerType === "CLUB_ADMIN") && source === "admin" &&
|
||||
<span>Tips 1: Il est possible de bannir un combattant, ce qui l'empêchera d'être réinscrit par un autre moyen que par un administrateur de cette compétition.
|
||||
Pour cela, cliquez sur la petite <FontAwesomeIcon icon={faGavel}/> à côté de son nom.<br/>
|
||||
Tips 2: Il est aussi possible de verrouiller les modifications de son inscription depuis sa fiche, ce qui l'empêchera d'être modifié/supprimé par lui-même et/ou un responsable de club.
|
||||
<span><Trans i18nKey="comp.tips"> petite <FontAwesomeIcon icon={faGavel}/> à</Trans>
|
||||
</span>}
|
||||
<div className="mb-4">
|
||||
<div className="list-group">
|
||||
@ -516,12 +516,12 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
<span className="col-auto">{req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"}</span>
|
||||
<div className="ms-2 col-auto">
|
||||
<div><strong>{req.data.fname} {req.data.lname}</strong> <small>{req.data.genre}</small></div>
|
||||
<small>{req.data.club?.name || "Sans club"}</small>
|
||||
<small>{req.data.club?.name || t("club", {count: 0})}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-auto" style={{textAlign: "right"}}>
|
||||
<small>{getCatName(req.data.categorie) + (req.data.overCategory === 0 ? "" : (" avec " + req.data.overCategory + " de surclassement"))}<br/>
|
||||
<small>{t("comp.surclassement", {count: req.data.overCategory, cat: getCatName(req.data.categorie)})}<br/>
|
||||
{req.data.weight ? req.data.weight : "---"} kg
|
||||
</small>
|
||||
</div>
|
||||
@ -535,16 +535,12 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
e.preventDefault()
|
||||
|
||||
if (req.data.lockEdit && source !== "admin") return;
|
||||
if (!window.confirm("Êtes-vous sûr de vouloir désinscrire et bannir ce combattant de la compétition?\n(Vous pouvez le réinscrire plus tard)"))
|
||||
if (!window.confirm(t('comp.warn1')))
|
||||
return;
|
||||
|
||||
toast.promise(apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=true`), {
|
||||
pending: "Désinscription en cours", success: "Combattant désinscrit et bannie", error: {
|
||||
render({data}) {
|
||||
return data.response.data || "Erreur"
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=true`), getToastMessage("comp.toast.register.ban")
|
||||
).finally(() => {
|
||||
dispatch({type: 'REMOVE', payload: req.id})
|
||||
})
|
||||
}}>
|
||||
@ -556,20 +552,16 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
|
||||
if (req.data.lockEdit && source !== "admin") return;
|
||||
if (registerType === "HELLOASSO") {
|
||||
if (!window.confirm("Êtes-vous sûr de vouloir désinscrire ce combattant ?\nCela ne le désinscrira pas de la billetterie HelloAsso et ne le remboursera pas."))
|
||||
if (!window.confirm(t('comp.warn2')))
|
||||
return;
|
||||
} else {
|
||||
if (!window.confirm("Êtes-vous sûr de vouloir désinscrire ce combattant ?"))
|
||||
if (!window.confirm(t('comp.warn3')))
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=false`), {
|
||||
pending: "Désinscription en cours", success: "Combattant désinscrit", error: {
|
||||
render({data}) {
|
||||
return data.response.data || "Erreur"
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=false`), getToastMessage("comp.toast.register.del")
|
||||
).finally(() => {
|
||||
dispatch({type: 'REMOVE', payload: req.id})
|
||||
})
|
||||
}}>
|
||||
@ -584,6 +576,7 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
}
|
||||
|
||||
function FileOutput({data}) {
|
||||
const {t} = useTranslation();
|
||||
const handleFileDownload = () => {
|
||||
const dataOut = []
|
||||
for (const e of data) {
|
||||
@ -612,7 +605,7 @@ function FileOutput({data}) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className="btn btn-primary" onClick={handleFileDownload}>Exporter les inscription</button>
|
||||
<button className="btn btn-primary" onClick={handleFileDownload}>{t('comp.exporterLesInscription')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,10 +4,12 @@ import {CompetitionList} from "./CompetitionList.jsx";
|
||||
import {CompetitionEdit} from "./CompetitionEdit.jsx";
|
||||
import {CompetitionRegisterAdmin} from "./CompetitionRegisterAdmin.jsx";
|
||||
import {CompetitionView} from "./CompetitionView.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function CompetitionRoot() {
|
||||
const {t} = useTranslation();
|
||||
return <>
|
||||
<h1>Compétition</h1>
|
||||
<h1>{t('compétition')}</h1>
|
||||
<LoadingProvider>
|
||||
<Outlet/>
|
||||
</LoadingProvider>
|
||||
|
||||
@ -3,10 +3,12 @@ import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {useAuth} from "../../hooks/useAuth.jsx";
|
||||
import {apiAxios, isClubAdmin} from "../../utils/Tools.js";
|
||||
import {apiAxios, getToastMessage, isClubAdmin} from "../../utils/Tools.js";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
export function CompetitionView() {
|
||||
|
||||
@ -32,13 +34,13 @@ export function CompetitionView() {
|
||||
|
||||
const inscText = (type) => {
|
||||
if (type === "FREE") {
|
||||
return "Libres"
|
||||
return i18n.t('comp.reg.libres')
|
||||
} else if (type === "CLUB_ADMIN") {
|
||||
return "Par les responsables de club"
|
||||
return i18n.t('comp.reg.parLesResponsablesDeClub')
|
||||
} else if (type === "ADMIN") {
|
||||
return "Par les administrateurs de la compétition"
|
||||
return i18n.t('comp.reg.parLesAdministrateursDeLaCompétition')
|
||||
} else if (type === "HELLOASSO") {
|
||||
return "Sur la billetterie HelloAsso"
|
||||
return i18n.t('comp.reg.surLaBilletterieHelloasso')
|
||||
}
|
||||
|
||||
return ""
|
||||
@ -47,6 +49,7 @@ const inscText = (type) => {
|
||||
function MakeContent({data}) {
|
||||
const {userinfo} = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">
|
||||
@ -54,35 +57,35 @@ function MakeContent({data}) {
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p>{data.description}</p>
|
||||
<p><strong>Date
|
||||
:</strong> Du {new Date(data.date.split('T')[0]).toLocaleDateString()} au {new Date(data.toDate.split('T')[0]).toLocaleDateString()}
|
||||
<p><strong>{t('date')}
|
||||
:</strong> {t('du')} {new Date(data.date.split('T')[0]).toLocaleDateString()} {t('au')} {new Date(data.toDate.split('T')[0]).toLocaleDateString()}
|
||||
</p>
|
||||
<p><strong>Lieu :</strong> {data.adresse}</p>
|
||||
<p><strong>Organisateur :</strong> {data.clubName}</p>
|
||||
<p><strong>Type d'inscription :</strong> {inscText(data.registerMode)}</p>
|
||||
<p><strong>{t('lieu')} :</strong> {data.adresse}</p>
|
||||
<p><strong>{t('comp.organisateur')} :</strong> {data.clubName}</p>
|
||||
<p><strong>{t('comp.typeDinscription')} :</strong> {inscText(data.registerMode)}</p>
|
||||
{(data.registerMode === "FREE" || data.registerMode === "CLUB_ADMIN") &&
|
||||
<p><strong>Date d'inscription
|
||||
:</strong> Du {new Date(data.startRegister.split('+')[0]).toLocaleString()} au {new Date(data.endRegister.split('+')[0]).toLocaleString()}
|
||||
<p><strong>{t('comp.dateDinscription')}
|
||||
:</strong> {t('du')} {new Date(data.startRegister.split('+')[0]).toLocaleString()} {t('au')} {new Date(data.endRegister.split('+')[0]).toLocaleString()}
|
||||
</p>
|
||||
}
|
||||
|
||||
{(data.registerMode === "FREE" || data.registerMode === "CLUB_ADMIN") && isClubAdmin(userinfo) &&
|
||||
<button type="button" className="btn btn-primary"
|
||||
disabled={new Date() < new Date(data.startRegister.split('+')[0]) || new Date() > new Date(data.endRegister.split('+')[0])}
|
||||
onClick={_ => navigate("/competition/" + data.id + "/club/register")}>Inscription</button>
|
||||
onClick={_ => navigate("/competition/" + data.id + "/club/register")}>{t('comp.inscription')}</button>
|
||||
}
|
||||
{data.registerMode === "FREE" && !isClubAdmin(userinfo) &&
|
||||
<SelfRegister data2={data}/>
|
||||
}
|
||||
{data.registerMode === "HELLOASSO" &&
|
||||
<p><strong>Billetterie :</strong> <a
|
||||
<p><strong>{t('comp.billetterie')} :</strong> <a
|
||||
href={`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`} target="_blank"
|
||||
rel="noopener noreferrer">{`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`}</a></p>
|
||||
}
|
||||
{data.canEditRegisters &&
|
||||
<div style={{marginTop: "0.5em"}}>
|
||||
<button type="button" className="btn btn-primary"
|
||||
onClick={_ => navigate("/competition/" + data.id + "/register")}>Inscription - mode administrateur
|
||||
onClick={_ => navigate("/competition/" + data.id + "/register")}>{t('comp.inscriptionModeAdministrateur')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@ -94,6 +97,7 @@ function SelfRegister({data2}) {
|
||||
const {id} = useParams()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/competition/${id}/register/user`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [weight, setWeight] = useState("")
|
||||
const [cat, setCat] = useState(0)
|
||||
@ -108,36 +112,22 @@ function SelfRegister({data2}) {
|
||||
const disabled = new Date() < new Date(data2.startRegister.split('+')[0]) || new Date() > new Date(data2.endRegister.split('+')[0])
|
||||
|
||||
const handleUnregister = () => {
|
||||
if (window.confirm("Êtes-vous sûr de vouloir vous désinscrire ?")) {
|
||||
toast.promise(apiAxios.delete(`/competition/${id}/register/${data[0].id}/user`), {
|
||||
pending: "Désinscription en cours",
|
||||
success: "Désinscription réalisée",
|
||||
error: {
|
||||
render({data}) {
|
||||
return data.response.data || "Erreur"
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
if (window.confirm(t('comp.warn4'))) {
|
||||
toast.promise(apiAxios.delete(`/competition/${id}/register/${data[0].id}/user`), getToastMessage("comp.toast.register.self.del")
|
||||
).finally(() => {
|
||||
refresh(`/competition/${id}/register/user`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sendSubmit = (new_state) => {
|
||||
toast.promise(apiAxios.post(`/competition/${id}/register/user`, new_state), {
|
||||
pending: "Enregistrement en cours",
|
||||
success: "Inscription réalisée",
|
||||
error: {
|
||||
render({data}) {
|
||||
return data.response.data || "Combattant non trouvé"
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
toast.promise(apiAxios.post(`/competition/${id}/register/user`, new_state), getToastMessage("comp.toast.register.self.add")
|
||||
).finally(() => {
|
||||
refresh(`/competition/${id}/register/user`)
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const handleSubmit = () => {
|
||||
sendSubmit({
|
||||
licence: 0, fname: "", lname: "", weight: weight, overCategory: cat, lockEdit: false, id: null
|
||||
})
|
||||
@ -147,34 +137,34 @@ function SelfRegister({data2}) {
|
||||
{data
|
||||
? data.length > 0
|
||||
? <div style={{textAlign: "right", maxWidth: "20em"}}>
|
||||
<h4 style={{textAlign: "left"}}>Mon inscription</h4>
|
||||
<h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="weight">Poids (en kg)</span>
|
||||
<span className="input-group-text" id="weight">{t("comp.modal.poids")}</span>
|
||||
<input type="number" min={1} step={1} className="form-control" placeholder="42" aria-label="weight" disabled={disabled}
|
||||
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div style={{textAlign: "left"}}>Catégorie normalisée: {data[0].categorie}</div>
|
||||
<div style={{textAlign: "left"}}>{t('comp.catégorieNormalisée')}: {data[0].categorie}</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="categorie">Surclassement</span>
|
||||
<span className="input-group-text" id="categorie">{t("comp.modal.surclassement")}</span>
|
||||
<select className="form-select" aria-label="categorie" name="categorie" value={cat} disabled={disabled}
|
||||
onChange={e => setCat(Number(e.target.value))}>
|
||||
<option value={0}>Aucun</option>
|
||||
<option value={1}>+1 catégorie</option>
|
||||
<option value={2}>+2 catégorie</option>
|
||||
<option value={0}>{t('aucun')}</option>
|
||||
<option value={1}>{t('1Catégorie')}</option>
|
||||
<option value={2}>{t('2Catégorie')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" className="btn btn-danger" disabled={disabled} style={{marginRight: "0.5em"}}
|
||||
onClick={handleUnregister}>Se désinscrire
|
||||
onClick={handleUnregister}>{t('button.seDésinscrire')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" disabled={disabled}
|
||||
onClick={handleSubmit}>Enregister
|
||||
onClick={handleSubmit}>{t('button.enregister')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
: <button type="button" className="btn btn-primary" disabled={disabled} onClick={handleSubmit}>S'inscrire</button>
|
||||
: <button type="button" className="btn btn-primary" disabled={disabled} onClick={handleSubmit}>{t('comp.sinscrire')}</button>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
|
||||
@ -12,6 +12,9 @@ import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
|
||||
import JSZip from "jszip";
|
||||
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||
import {faGlobe} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import {getToastMessage} from "../../../utils/Tools.js";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -139,7 +142,7 @@ async function downloadResourcesAsZip(resourceList) {
|
||||
completed++;
|
||||
const progress = Math.round((completed / resourceList.length) * 100);
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`;
|
||||
progressText.textContent = `${i18n.t('téléchargement')} (${completed}/${resourceList.length}) : ${result.filename}`;
|
||||
return result;
|
||||
})
|
||||
);
|
||||
@ -155,13 +158,14 @@ async function downloadResourcesAsZip(resourceList) {
|
||||
|
||||
// Fermer la modale
|
||||
modal.hide();
|
||||
progressText.textContent = "Téléchargement terminé !";
|
||||
progressText.textContent = i18n.t('téléchargementTerminé!');
|
||||
}
|
||||
|
||||
function Menu({menuActions, compUuid}) {
|
||||
const e = document.getElementById("actionMenu")
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
for (const x of tto)
|
||||
x.dispose();
|
||||
@ -217,12 +221,16 @@ function Menu({menuActions, compUuid}) {
|
||||
}
|
||||
|
||||
const copyScriptToClipboard = () => {
|
||||
navigator.clipboard.writeText(`<div id='safca_api_data'></div>
|
||||
<script id="safca_api_script" type="text/javascript" src="${vite_url}/competition.js?id=${compUuid}"></script>`
|
||||
navigator.clipboard.writeText(`<!--suppress ALL -->
|
||||
<div id='safca_api_data'></div>
|
||||
<script type="module">
|
||||
import {initCompetitionApi} from '${vite_url}/competition.js';
|
||||
initCompetitionApi("${vite_url}/api/public/result/${compUuid}")
|
||||
</script>`
|
||||
).then(() => {
|
||||
toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.");
|
||||
toast.success(t('texteCopiéDansLePresse'));
|
||||
}).catch(err => {
|
||||
toast.error("Erreur lors de la copie dans le presse-papier : " + err);
|
||||
toast.error(t('erreurLorsDeLaCopieDansLePresse') + err);
|
||||
});
|
||||
}
|
||||
|
||||
@ -237,12 +245,12 @@ function Menu({menuActions, compUuid}) {
|
||||
onMouseDown={() => longPressDown("obs")}
|
||||
onMouseUp={() => longPressUp("obs")}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/>
|
||||
data-bs-title={t('ttm.admin.obs')}/>
|
||||
<FontAwesomeIcon icon={faGlobe} size="xl"
|
||||
style={{color: "#6c757d", cursor: "pointer"}}
|
||||
onClick={() => copyScriptToClipboard()}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title="Copier le scripte d'intégration"/>
|
||||
data-bs-title={t('ttm.admin.scripte')}/>
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
||||
@ -252,33 +260,32 @@ function Menu({menuActions, compUuid}) {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Configuration OBS</h5>
|
||||
<h5 className="modal-title">{t('configurationObs')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form onSubmit={handleOBSSubmit}>
|
||||
<div className="modal-body">
|
||||
<strong>/!\ Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en
|
||||
changer entre chaque compétition</strong>
|
||||
<strong>{t('config.obs.warn1')}</strong>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Adresse du serveur</span>
|
||||
<span className="input-group-text">ws://</span>
|
||||
<span className="input-group-text">{t('adresseDuServeur')}</span>
|
||||
<span className="input-group-text">{t('config.obs.ws')}</span>
|
||||
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
|
||||
defaultValue={"127.0.0.1:4455"}/>
|
||||
<span className="input-group-text">/</span>
|
||||
</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Mot de passe du serveur</span>
|
||||
<span className="input-group-text">{t('config.obs.motDePasseDuServeur')}</span>
|
||||
<input type="password" className="form-control" placeholder="12345" aria-label=""
|
||||
defaultValue={""}/>
|
||||
</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Dossier des resources</span>
|
||||
<span className="input-group-text">{t('config.obs.dossierDesResources')}</span>
|
||||
<input type="text" className="form-control" placeholder="" aria-label="" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Exporter</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('exporter')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -289,14 +296,14 @@ function Menu({menuActions, compUuid}) {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Téléchargement en cours...</h5>
|
||||
<h5 className="modal-title">{t('téléchargementEnCours')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="progress">
|
||||
<div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div>
|
||||
</div>
|
||||
<div id="progressText" className="mt-2">Préparation...</div>
|
||||
<div id="progressText" className="mt-2">{t('préparation...')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -310,6 +317,7 @@ function CategoryHeader({cat, setCatId}) {
|
||||
const confirmRef = useRef();
|
||||
const [modal, setModal] = useState({})
|
||||
const [confirm, setConfirm] = useState({})
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
||||
const {dispatch} = useWS();
|
||||
@ -364,17 +372,17 @@ function CategoryHeader({cat, setCatId}) {
|
||||
return <div className="row">
|
||||
<div className="col-auto">
|
||||
<div className="input-group">
|
||||
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5>
|
||||
<h5 style={{margin: "auto 0.5em auto 0"}}>{t('editionDeLaCatégorie')}</h5>
|
||||
<select className="form-select" onChange={handleCatChange} value={cat?.id || ""}>
|
||||
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>))}
|
||||
{cats && <option value={-1}>Nouvelle...</option>}
|
||||
{cats && <option value={-1}>{t('nouvelle...')}</option>}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col" style={{margin: "auto 0", textAlign: "center"}}>
|
||||
{cat &&
|
||||
<div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} |
|
||||
<div>Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? t('tournois') : ""} |
|
||||
Zone: {cat.liceName}</div>}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
@ -405,17 +413,18 @@ function CategoryHeader({cat, setCatId}) {
|
||||
|
||||
function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
const [name, setName] = useState("")
|
||||
const [lice, setLice] = useState("A")
|
||||
const [lice, setLice] = useState("1")
|
||||
const [poule, setPoule] = useState(true)
|
||||
const [tournoi, setTournoi] = useState(false)
|
||||
const [size, setSize] = useState(4)
|
||||
const [loserMatch, setLoserMatch] = useState(1)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const {sendRequest} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setName(state.name || "");
|
||||
setLice(state.liceName || "A");
|
||||
setLice(state.liceName || "1");
|
||||
setPoule(((state.type || 1) & 1) !== 0);
|
||||
setTournoi((state.type & 2) !== 0);
|
||||
|
||||
@ -441,13 +450,13 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
|
||||
const regex = /^([^;]+;)*[^;]+$/;
|
||||
if (regex.test(lice.trim()) === false) {
|
||||
toast.error("Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.");
|
||||
toast.error(t('err2'));
|
||||
return;
|
||||
}
|
||||
|
||||
const nType = (poule ? 1 : 0) + (tournoi ? 2 : 0);
|
||||
if (nType === 0) {
|
||||
toast.error("Au moins un type (poule ou tournoi) doit être sélectionné.");
|
||||
toast.error(t('err3'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -475,8 +484,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
console.log(tournoi, size, nbMatch, loserMatch, oldSubTree);
|
||||
if (tournoi && (size !== nbMatch * 2 || loserMatch !== oldSubTree)) {
|
||||
setConfirm({
|
||||
title: "Changement de l'arbre du tournoi",
|
||||
message: `Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!`,
|
||||
title: t('confirm2.title'),
|
||||
message: t('confirm2.msg'),
|
||||
confirm: () => {
|
||||
const trees2 = build_tree(size, loserMatch)
|
||||
const newTrees = []
|
||||
@ -491,48 +500,30 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
newTrees.push(trees2.at(i));
|
||||
}
|
||||
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}),
|
||||
{
|
||||
pending: 'Mise à jour des arbres du tournoi...',
|
||||
success: 'Arbres mis à jour !',
|
||||
error: 'Erreur lors de la mise à jour des arbres'
|
||||
}
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees")
|
||||
).then(__ => {
|
||||
toast.promise(sendRequest('updateCategory', newData),
|
||||
{
|
||||
pending: 'Mise à jour de la catégorie...',
|
||||
success: 'Catégorie mise à jour !',
|
||||
error: 'Erreur lors de la mise à jour de la catégorie'
|
||||
}
|
||||
)
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory"))
|
||||
})
|
||||
}
|
||||
})
|
||||
confirmRef.current.click();
|
||||
} else {
|
||||
toast.promise(sendRequest('updateCategory', newData),
|
||||
{
|
||||
pending: 'Mise à jour de la catégorie...',
|
||||
success: 'Catégorie mise à jour !',
|
||||
error: 'Erreur lors de la mise à jour de la catégorie'
|
||||
}
|
||||
)
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory"))
|
||||
}
|
||||
}
|
||||
|
||||
if (nType !== state.type) {
|
||||
let typeStr = "";
|
||||
if ((state.type & 1) !== 0 && (nType & 1) === 0)
|
||||
typeStr += "poule ";
|
||||
typeStr += `${t('poule').toLowerCase()} `;
|
||||
if ((state.type & 2) !== 0 && (nType & 2) === 0)
|
||||
typeStr += "tournoi ";
|
||||
typeStr += `${t('tournoi').toLowerCase()} `;
|
||||
|
||||
setConfirm({
|
||||
title: "Changement de type de catégorie",
|
||||
message: `Voulez-vous vraiment enlever la partie ${typeStr} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !`,
|
||||
title: t('confirm3.title'),
|
||||
message: t('confirm3.msg', {typeStr: typeStr.trim()}),
|
||||
confirm: () => {
|
||||
setTimeout(() =>
|
||||
applyChanges(), 500);
|
||||
setTimeout(() => applyChanges(), 500);
|
||||
}
|
||||
})
|
||||
confirmRef.current.click();
|
||||
@ -540,23 +531,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
applyChanges();
|
||||
}
|
||||
} else {
|
||||
toast.promise(sendRequest('createCategory', {name: name.trim(), liceName: lice.trim(), type: nType}),
|
||||
{
|
||||
pending: 'Création de la catégorie...',
|
||||
success: 'Catégorie créée !',
|
||||
error: 'Erreur lors de la création de la catégorie'
|
||||
}
|
||||
toast.promise(sendRequest('createCategory', {
|
||||
name: name.trim(),
|
||||
liceName: lice.trim(),
|
||||
type: nType
|
||||
}), getToastMessage("toast.createCategory")
|
||||
).then(id => {
|
||||
if (tournoi) {
|
||||
const trees = build_tree(size, loserMatch)
|
||||
console.log("Creating trees for new category:", trees);
|
||||
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}),
|
||||
{
|
||||
pending: 'Création des arbres du tournoi...',
|
||||
success: 'Arbres créés !',
|
||||
error: 'Erreur lors de la création des arbres'
|
||||
}
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init")
|
||||
).finally(() => setCatId(id))
|
||||
} else {
|
||||
setCatId(id);
|
||||
@ -576,36 +561,36 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? "Ajouter" : "Modifier"} une catégorie</h1>
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? t('ajouter') : t('modifier')} {t('uneCatégorie')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label htmlFor="nameInput1" className="form-label">Nom</label>
|
||||
<input type="text" className="form-control" id="nameInput1" placeholder="Epée bouclier" name="name" value={name}
|
||||
<label htmlFor="nameInput1" className="form-label">{t('nom')}</label>
|
||||
<input type="text" className="form-control" id="nameInput1" placeholder={t('epéeBouclier')} name="name" value={name}
|
||||
onChange={e => setName(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="liceInput1" className="form-label">Nom des zones de combat <small>(séparée par des ';')</small></label>
|
||||
<input type="text" className="form-control" id="liceInput1" placeholder="A;B" name="zone de combat" value={lice}
|
||||
<label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
|
||||
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice}
|
||||
onChange={e => setLice(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Type</label>
|
||||
<label className="form-label">{t('type')}</label>
|
||||
<div className="form-check form-switch">
|
||||
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault" name="poule" checked={poule}
|
||||
onChange={e => setPoule(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="switchCheckDefault">Poule</label>
|
||||
<label className="form-check-label" htmlFor="switchCheckDefault">{t('poule')}</label>
|
||||
</div>
|
||||
|
||||
<div className="form-check form-switch">
|
||||
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault2" name="trournoi" checked={tournoi}
|
||||
onChange={e => setTournoi(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="switchCheckDefault2">Tournoi</label>
|
||||
<label className="form-check-label" htmlFor="switchCheckDefault2">{t('tournoi')}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -616,14 +601,14 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<span>Match pour les perdants du tournoi:</span>
|
||||
<span>{t('matchPourLesPerdantsDuTournoi')}</span>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault1" disabled={!tournoi}
|
||||
checked={loserMatch === -1} onChange={e => {
|
||||
if (e.target.checked) setLoserMatch(-1)
|
||||
}}/>
|
||||
<label className="form-check-label" htmlFor="radioDefault1">
|
||||
Tous les matchs
|
||||
{t('tousLesMatchs')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
@ -632,7 +617,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
if (e.target.checked) setLoserMatch(1)
|
||||
}}/>
|
||||
<label className="form-check-label" htmlFor="radioDefault2">
|
||||
Demi-finales et finales
|
||||
{t('demi-finalesEtFinales')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
@ -641,31 +626,26 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
if (e.target.checked) setLoserMatch(0)
|
||||
}}/>
|
||||
<label className="form-check-label" htmlFor="radioDefault3">
|
||||
Finales uniquement
|
||||
{t('finalesUniquement')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('enregistrer')}</button>
|
||||
{state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
|
||||
setConfirm({
|
||||
title: "Suppression de la catégorie",
|
||||
message: `Voulez-vous vraiment supprimer la catégorie ${state.name}. Cela va supprimer tous les matchs associés !`,
|
||||
title: t('confirm4.title'),
|
||||
message: t('confirm4.msg', {name: state.name}),
|
||||
confirm: () => {
|
||||
toast.promise(sendRequest('deleteCategory', state.id),
|
||||
{
|
||||
pending: 'Suppression de la catégorie...',
|
||||
success: 'Catégorie supprimée !',
|
||||
error: 'Erreur lors de la suppression de la catégorie'
|
||||
}
|
||||
toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory")
|
||||
).then(() => setCatId(null));
|
||||
}
|
||||
})
|
||||
confirmRef.current.click();
|
||||
}}>Supprimer</button>}
|
||||
}}>{t('supprimer')}</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {timePrint} from "../../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ChronoPanel() {
|
||||
const [config, setConfig] = useState({
|
||||
@ -11,6 +12,7 @@ export function ChronoPanel() {
|
||||
const chronoText = useRef(null)
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time}))
|
||||
const isRunning = () => chrono.startTime !== 0
|
||||
@ -111,11 +113,11 @@ export function ChronoPanel() {
|
||||
onClick={__ => isRunning() ?
|
||||
setChrono(prev => ({...prev, time: prev.time + Date.now() - prev.startTime, startTime: 0})) :
|
||||
setChrono(prev => ({...prev, startTime: Date.now()}))}>
|
||||
{isRunning() ? "Arrêter" : "Démarrer"}</button>
|
||||
{isRunning() ? t('chrono.arrêter') : t('chrono.démarrer')}</button>
|
||||
<button className="btn btn-danger col" onClick={__ => {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: 0}))
|
||||
state.current.chronoState = 0
|
||||
}}>Réinitialiser
|
||||
}}>{t('réinitialiser')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "0.5em"}}>
|
||||
@ -125,29 +127,29 @@ export function ChronoPanel() {
|
||||
</div>
|
||||
<div className="col" style={{margin: "auto 0"}}>
|
||||
<div className="row">
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-10000)}>-10 s</button>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(10000)}>+10 s</button>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-10000)}>{t('chrono.-10S')}</button>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(10000)}>{t('chrono.+10S')}</button>
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "0.5em"}}>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-1000)}>-1 s</button>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(1000)}>+1 s</button>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-1000)}>{t('chrono.-1S')}</button>
|
||||
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(1000)}>{t('chrono.+1S')}</button>
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "0.5em"}}>
|
||||
<button className="btn btn-outline-secondary col-12" onClick={__ => {
|
||||
const timeStr = prompt("Entrez le temps en s", "0");
|
||||
const timeStr = prompt(t('chrono.entrezLeTempsEnS'), "0");
|
||||
if (timeStr === null)
|
||||
return;
|
||||
addTime(parseInt(timeStr, 10) * 1000);
|
||||
}}>+/- ... s
|
||||
}}>{t('chrono.+/-...S')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "0.5em"}}>
|
||||
<div className="col-12 col-sm-8" style={{margin: 'auto 0'}}>
|
||||
<div>Temps: {timePrint(config.time)}, pause: {timePrint(config.pause)}</div>
|
||||
<div>{t('chrono.recapTemps', {temps: timePrint(config.time), pause: timePrint(config.pause)})}</div>
|
||||
</div>
|
||||
<button className="btn btn-secondary col" data-bs-toggle="modal" data-bs-target="#timeModal">Définir le temps
|
||||
<button className="btn btn-secondary col" data-bs-toggle="modal" data-bs-target="#timeModal">{t('chrono.définirLeTemps')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -155,25 +157,25 @@ export function ChronoPanel() {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Edition temps</h5>
|
||||
<h5 className="modal-title">{t('chrono.editionTemps')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Durée round</span>
|
||||
<span className="input-group-text">{t('duréeRound')}</span>
|
||||
<input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.time)}/>
|
||||
<span className="input-group-text">(mm:ss)</span>
|
||||
</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Durée pause</span>
|
||||
<span className="input-group-text">{t('duréePause')}</span>
|
||||
<input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.pause)}/>
|
||||
<span className="input-group-text">(mm:ss)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Valider</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('valider')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -6,12 +6,13 @@ import {DrawGraph} from "../../result/DrawGraph.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
||||
import {scorePrint, win} from "../../../utils/Tools.js";
|
||||
import {getToastMessage, scorePrint, win} from "../../../utils/Tools.js";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
||||
import {toast} from "react-toastify";
|
||||
import "./CMTMatchPanel.css"
|
||||
import {useOBS} from "../../../hooks/useOBS.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function CupImg() {
|
||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||
@ -24,6 +25,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
|
||||
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
||||
const {dispatch} = useWS();
|
||||
const {connected, setText} = useOBS();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
const categoryListener = ({data}) => {
|
||||
@ -55,7 +57,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
|
||||
|
||||
return <>
|
||||
<div className="input-group">
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>{t('catégorie')}</h6>
|
||||
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
|
||||
{cats && <option value={-1}></option>}
|
||||
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
|
||||
@ -149,6 +151,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
|
||||
function ListMatch({cat, matches, trees, menuActions}) {
|
||||
const [type, setType] = useState(1);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
if (!cat)
|
||||
@ -165,12 +168,12 @@ function ListMatch({cat, matches, trees, menuActions}) {
|
||||
<ul className="nav nav-tabs">
|
||||
<li className="nav-item">
|
||||
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
|
||||
onClick={_ => setType(1)}>Poule
|
||||
onClick={_ => setType(1)}>{t('poule')}
|
||||
</div>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
|
||||
onClick={_ => setType(2)}>Tournois
|
||||
onClick={_ => setType(2)}>{t('tournois')}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -189,8 +192,9 @@ function ListMatch({cat, matches, trees, menuActions}) {
|
||||
|
||||
function MatchList({matches, cat, menuActions}) {
|
||||
const [activeMatch, setActiveMatch] = useState(null)
|
||||
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "A")
|
||||
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const liceName = (cat.liceName || "N/A").split(";");
|
||||
const marches2 = matches.filter(m => m.categorie_ord !== -42)
|
||||
@ -241,7 +245,7 @@ function MatchList({matches, cat, menuActions}) {
|
||||
return <>
|
||||
{liceName.length > 1 &&
|
||||
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
|
||||
<label className="input-group-text" htmlFor="selectLice">Zone de combat</label>
|
||||
<label className="input-group-text" htmlFor="selectLice">{t('zoneDeCombat')}</label>
|
||||
<select className="form-select" id="selectLice" value={lice} onChange={e => {
|
||||
setLice(e.target.value);
|
||||
localStorage.setItem("cm_lice", e.target.value);
|
||||
@ -259,10 +263,10 @@ function MatchList({matches, cat, menuActions}) {
|
||||
<tr>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col">Rouge</th>
|
||||
<th style={{textAlign: "center"}} scope="col">Blue</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -288,7 +292,8 @@ function MatchList({matches, cat, menuActions}) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
{activeMatch &&
|
||||
<LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
</>
|
||||
}
|
||||
|
||||
@ -362,7 +367,8 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
</div>
|
||||
|
||||
{currentMatch?.matchSelect &&
|
||||
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match}
|
||||
menuActions={menuActions}/></LoadingProvider>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -379,6 +385,7 @@ function ScorePanel({matchId, matchs, match, menuActions}) {
|
||||
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
const {sendRequest} = useWS()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const [end, setEnd] = useState(match?.end || false)
|
||||
const [scoreIn, setScoreIn] = useState("")
|
||||
@ -396,13 +403,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
menuActions.current.saveScore = (scoreRed, scoreBlue) => {
|
||||
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
||||
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
|
||||
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}),
|
||||
{
|
||||
pending: 'Sauvegarde du score...',
|
||||
success: 'Score sauvegardé !',
|
||||
error: 'Erreur lors de la sauvegarde du score'
|
||||
}
|
||||
);
|
||||
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore"));
|
||||
}
|
||||
return () => menuActions.current.saveScore = undefined;
|
||||
}, [matchId])
|
||||
@ -443,8 +444,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
|
||||
const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round);
|
||||
|
||||
console.log("Updating score", matchId, round, comb, scoreIn_, score);
|
||||
|
||||
let newScore;
|
||||
if (score) {
|
||||
if (comb === 1)
|
||||
@ -460,8 +459,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Updating score", matchId, newScore);
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
|
||||
.finally(() => {
|
||||
@ -485,7 +482,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
|
||||
if (end) {
|
||||
if (win(match?.scores) === 0 && match.categorie_ord === -42) {
|
||||
toast.error("Impossible de terminer un match nul en tournois.");
|
||||
toast.error(t('score.err1'));
|
||||
setEnd(false);
|
||||
return;
|
||||
}
|
||||
@ -529,21 +526,18 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
const o = [...tooltipTriggerList]
|
||||
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
|
||||
const tt = "Score speciaux : <br/>" +
|
||||
"-997 : disqualifié <br/>" +
|
||||
"-998 : absent <br/>" +
|
||||
"-999 : forfait"
|
||||
const tt = t('score.spe')
|
||||
|
||||
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
|
||||
return <div ref={tableRef} className="col" style={{position: "relative"}}>
|
||||
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
|
||||
<h6>{t('scores')} <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
|
||||
data-bs-html="true"/></h6>
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "center"}} scope="col">Manche</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('manche')}</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('rouge')}</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('bleu')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-group-divider">
|
||||
@ -568,7 +562,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
<div className="form-check" style={{display: "inline-block"}}>
|
||||
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
|
||||
onChange={e => setEnd(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkboxEnd">Terminé</label>
|
||||
<label className="form-check-label" htmlFor="checkboxEnd">{t('terminé')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
|
||||
|
||||
@ -2,12 +2,14 @@ import {useEffect, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function PointPanel({menuActions}) {
|
||||
const [revers, setRevers] = useState(false)
|
||||
const [scoreRouge, setScoreRouge] = useState(0)
|
||||
const [scoreBleu, setScoreBleu] = useState(0)
|
||||
const publicAffDispatch = usePubAffDispatch()
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
menuActions.current.switchSore = () => {
|
||||
setRevers(!revers)
|
||||
@ -44,8 +46,8 @@ export function PointPanel({menuActions}) {
|
||||
{revers && red}
|
||||
|
||||
<div className="col row align-items-center">
|
||||
<button className="btn btn-danger" onClick={handleReset}>Réinitialiser</button>
|
||||
<button className="btn btn-success" onClick={handleSave}>Sauvegarder</button>
|
||||
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
|
||||
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -14,12 +14,14 @@ import {PointPanel} from "./CMTPoint.jsx";
|
||||
import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx";
|
||||
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
|
||||
import {toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function CMTable() {
|
||||
const combDispatch = useCombsDispatch()
|
||||
const [catId, setCatId] = useState(-1);
|
||||
const menuActions = useRef({});
|
||||
const {data} = useRequestWS("getRegister", null)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
if (data === null)
|
||||
@ -33,14 +35,14 @@ export function CMTable() {
|
||||
<div className="row">
|
||||
<div className="col-md-12 col-lg">
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">Chronomètre</div>
|
||||
<div className="card-header">{t('chronomètre')}</div>
|
||||
<div className="card-body">
|
||||
<ChronoPanel/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">Score</div>
|
||||
<div className="card-header">{t('score')}</div>
|
||||
<div className="card-body">
|
||||
<PointPanel menuActions={menuActions}/>
|
||||
</div>
|
||||
@ -48,7 +50,7 @@ export function CMTable() {
|
||||
</div>
|
||||
<div className="col-md-12 col-xl-6 col-xxl-5">
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">Matches</div>
|
||||
<div className="card-header">{t('matches')}</div>
|
||||
<div className="card-body">
|
||||
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
|
||||
</div>
|
||||
@ -74,6 +76,7 @@ function Menu({menuActions}) {
|
||||
const {connected, connect, disconnect} = useOBS();
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const externalWindow = useRef(null)
|
||||
const containerEl = useRef(document.createElement("div"))
|
||||
@ -154,7 +157,7 @@ function Menu({menuActions}) {
|
||||
connect("ws://" + config.adresse + "/", config.password, config.assets_dir);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Aucune configuration OBS trouvée, veuillez en importer une");
|
||||
toast.error(t('aucuneConfigurationObs'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -182,21 +185,21 @@ function Menu({menuActions}) {
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title="Inverser la position des combattants sur cette écran"/>
|
||||
data-bs-title={t('ttm.table.inverserLaPosition')}/>
|
||||
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
|
||||
style={{color: connected ? "#00c700" : "#6c757d", cursor: "pointer"}}
|
||||
onMouseDown={() => longPressDown("obs")}
|
||||
onMouseUp={() => longPressUp("obs")}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title="Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice"/>
|
||||
data-bs-title={t('ttm.table.obs')}/>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<FontAwesomeIcon icon={faDisplay} size="xl"
|
||||
style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={handlePubAff}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Ouvrir l'affichage public"/>
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.table.pub_aff')}/>
|
||||
<FontAwesomeIcon icon={SimpleIconsScore} size="xl" style={{color: showScore ? "#00c700" : "#6c757d", cursor: "pointer"}}
|
||||
onClick={handleScore}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Afficher les scores sur l'affichage public"/>
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.table.pub_score')}/>
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||
|
||||
@ -213,15 +216,15 @@ function Menu({menuActions}) {
|
||||
<form onSubmit={handleOBSSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Préfix des sources</span>
|
||||
<span className="input-group-text">{t('obs.préfixDesSources')}</span>
|
||||
<span className="input-group-text">sub</span>
|
||||
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
|
||||
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Sauvegarde</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('sauvegarde')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,8 @@ import {CSS} from '@dnd-kit/utilities';
|
||||
import {toast} from "react-toastify";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {win} from "../../../utils/Tools.js";
|
||||
import {getToastMessage, win} from "../../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -169,6 +170,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
|
||||
const combDispatch = useCombsDispatch()
|
||||
const {dispatch} = useWS()
|
||||
const [modalId, setModalId] = useState(null)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
const sendRegister = ({data}) => {
|
||||
@ -218,7 +220,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
|
||||
return <>
|
||||
<GroupsList groups={groups} setModalId={setModalId}/>
|
||||
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
|
||||
disabled={data === null}>Ajouter des combattants
|
||||
disabled={data === null}>{t('ajouterDesCombattants')}
|
||||
</button>
|
||||
|
||||
<div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true">
|
||||
@ -241,6 +243,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
|
||||
|
||||
function GroupsList({groups, setModalId}) {
|
||||
const {getComb} = useCombs();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const groups2 = groups.map(g => {
|
||||
const comb = getComb(g.id);
|
||||
@ -261,7 +264,7 @@ function GroupsList({groups, setModalId}) {
|
||||
return <>
|
||||
{Object.keys(groups2).map((poule) => (
|
||||
<div key={poule} className="mb-3">
|
||||
<h5>{poule !== '-' ? "Poule: " + poule : "Sans poule"}</h5>
|
||||
<h5>{poule !== '-' ? (t('poule') +" : " + poule) : t('sansPoule')}</h5>
|
||||
<ol className="list-group list-group-numbered">
|
||||
{groups2[poule].map((comb) => (
|
||||
<li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start"
|
||||
@ -279,6 +282,7 @@ function GroupsList({groups, setModalId}) {
|
||||
function GroupModalContent({combId, groups, setGroups, removeGroup}) {
|
||||
const inputRef = useRef(null);
|
||||
const [pouleInput, setPouleInput] = useState(groups.find(g => g.id === combId)?.poule || "")
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
setPouleInput(groups.find(g => g.id === combId)?.poule || "")
|
||||
@ -308,7 +312,7 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) {
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Poule pour: <CombName combId={combId}/></h5>
|
||||
<h5 className="modal-title">{t('poulePour')}<CombName combId={combId}/></h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
@ -319,11 +323,11 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) {
|
||||
}}/>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleClick}>Enregister</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleClick}>{t('enregister')}</button>
|
||||
<button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
|
||||
removeGroup(combId)
|
||||
}}>Supprimer
|
||||
}}>{t('supprimer')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@ -334,6 +338,7 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
const {sendRequest} = useWS();
|
||||
const [type, setType] = useState(1);
|
||||
const bthRef = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
if ((cat.type & type) === 0)
|
||||
@ -369,18 +374,13 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
|
||||
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
|
||||
matchesToRemove: matchesToRemove.map(m => m.id)
|
||||
}),
|
||||
{
|
||||
pending: 'Création des matchs en cours...',
|
||||
success: 'Matchs créés avec succès !',
|
||||
error: 'Erreur lors de la création des matchs',
|
||||
})
|
||||
}), getToastMessage("toast.matchs.create"))
|
||||
.finally(() => {
|
||||
console.log("Finished creating matches");
|
||||
})
|
||||
|
||||
} catch (e) {
|
||||
toast.error("Erreur lors de la création des matchs: " + e.message);
|
||||
toast.error(t('erreurLorsDeLaCréationDesMatchs') + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,12 +389,12 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
<ul className="nav nav-tabs">
|
||||
<li className="nav-item">
|
||||
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
|
||||
onClick={_ => setType(1)}>Poule
|
||||
onClick={_ => setType(1)}>{t('poule')}
|
||||
</div>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
|
||||
onClick={_ => setType(2)}>Tournois
|
||||
onClick={_ => setType(2)}>{t('tournois')}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -402,7 +402,7 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
|
||||
{type === 1 && <>
|
||||
<MatchList matches={matches} groups={groups} cat={cat} reducer={reducer}/>
|
||||
<button className="btn btn-primary float-end" onClick={handleCreatMatch}>Créer les matchs</button>
|
||||
<button className="btn btn-primary float-end" onClick={handleCreatMatch}>{t('créerLesMatchs')}</button>
|
||||
</>}
|
||||
|
||||
{type === 2 && <>
|
||||
@ -414,21 +414,21 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title" id="makeMatchModeLabel">Attention</h4>
|
||||
<h4 className="modal-title" id="makeMatchModeLabel">{t('attention')}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?
|
||||
{t('msg1')}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div className="btn-group" role="group" aria-label="Basic mixed styles example">
|
||||
<button className="btn btn-primary" data-dismiss="modal" data-bs-dismiss="modal" onClick={() => recalculateMatch(2)}>
|
||||
Tout conserver
|
||||
{t('toutConserver')}
|
||||
</button>
|
||||
<button className="btn btn-success" data-bs-dismiss="modal" onClick={() => recalculateMatch(1)}>
|
||||
Conserver uniquement les matchs terminés
|
||||
{t('conserverUniquementLesMatchsTerminés')}
|
||||
</button>
|
||||
<button className="btn btn-danger" data-bs-dismiss="modal" onClick={() => recalculateMatch(0)}>
|
||||
Ne rien conserver
|
||||
{t('neRienConserver')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -447,6 +447,7 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
const [combSelect, setCombSelect] = useState(0)
|
||||
const [combC1nm, setCombC1nm] = useState(null)
|
||||
const [combC2nm, setCombC2nm] = useState(null)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const liceName = (cat.liceName || "N/A").split(";");
|
||||
const marches2 = matches.filter(m => m.categorie_ord !== -42)
|
||||
@ -474,7 +475,7 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
if (!combC1nm || !combC2nm)
|
||||
return;
|
||||
if (combC1nm === combC2nm) {
|
||||
toast.error("Un combattant ne peut pas s'affronter lui-même !");
|
||||
toast.error(t('err1'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -525,7 +526,7 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
}
|
||||
|
||||
if (data.c1 === data.c2) {
|
||||
toast.error("Un combattant ne peut pas s'affronter lui-même !");
|
||||
toast.error(t('err1'));
|
||||
onClickVoid()
|
||||
return;
|
||||
}
|
||||
@ -543,7 +544,7 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
if (!match)
|
||||
return;
|
||||
|
||||
if (!confirm("Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?"))
|
||||
if (!confirm(t('confirm1')))
|
||||
return;
|
||||
|
||||
setLoading(1)
|
||||
@ -581,14 +582,14 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Poule</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Zone</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('poule')}</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('zone')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col">Rouge</th>
|
||||
<th style={{textAlign: "center"}} scope="col">Blue</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col">Résultat</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('résultat')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
</tr>
|
||||
@ -637,7 +638,7 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
|
||||
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
|
||||
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
|
||||
<option value={0}>-- Sélectionner un combattant --</option>
|
||||
<option value={0}>{t('--SélectionnerUnCombattant--')}</option>
|
||||
{combsIDs.map((combId) => (
|
||||
<option key={combId} value={combId}><CombName combId={combId}/></option>
|
||||
))}
|
||||
@ -683,6 +684,7 @@ function BuildTree({treeData, matches, groups}) {
|
||||
const {getComb} = useCombs()
|
||||
const {sendRequest} = useWS();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
function parseTree(data_in) {
|
||||
if (data_in?.data == null)
|
||||
@ -774,7 +776,7 @@ function BuildTree({treeData, matches, groups}) {
|
||||
<DrawGraph root={initTree(treeData)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}/>
|
||||
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
|
||||
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
|
||||
<option value={0}>-- Sélectionner un combattant --</option>
|
||||
<option value={0}>{t('--SélectionnerUnCombattant--')}</option>
|
||||
{combsIDs.map((combId) => (
|
||||
<option key={combId} value={combId}><CombName combId={combId}/></option>
|
||||
))}
|
||||
|
||||
@ -9,12 +9,14 @@ import {CMTable} from "./CMTable.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export default function CompetitionManagerRoot() {
|
||||
const {t} = useTranslation("cm");
|
||||
return <>
|
||||
<h1>Compétition manager</h1>
|
||||
<h1>{t('compétitionManager')}</h1>
|
||||
<LoadingProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home/>}/>
|
||||
@ -40,9 +42,10 @@ function Home() {
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, navigate}) {
|
||||
const {t} = useTranslation("cm");
|
||||
return <>
|
||||
<div className="mb-4">
|
||||
<h4>Compétition:</h4>
|
||||
<h4>{t('compétition')}:</h4>
|
||||
<div className="list-group">
|
||||
{data.sort((a, b) => new Date(b.date.split('T')[0]) - new Date(a.date.split('T')[0])).map((o) => (
|
||||
<li className="list-group-item list-group-item-action" key={o.id}
|
||||
@ -77,6 +80,7 @@ function HomeComp() {
|
||||
function WSStatus({setPerm}) {
|
||||
const [inWait, setInWait] = useState(false)
|
||||
const {isReady, wait_length, welcomeData} = useWS();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@ -91,7 +95,7 @@ function WSStatus({setPerm}) {
|
||||
|
||||
return <div className="row" style={{marginRight: "inherit"}}>
|
||||
<h2 className="col">{welcomeData.name}</h2>
|
||||
<div className="col-auto" style={{margin: "auto 0", padding: 0}}>Serveur: <ColoredCircle
|
||||
<div className="col-auto" style={{margin: "auto 0", padding: 0}}>{t('serveur')}: <ColoredCircle
|
||||
color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/>
|
||||
</div>
|
||||
<div className="col-auto " id="actionMenu" style={{verticalAlign: "center", textAlign: "center", margin: "auto 0", padding: 0}}></div>
|
||||
@ -100,16 +104,17 @@ function WSStatus({setPerm}) {
|
||||
|
||||
function Home2({perm}) {
|
||||
const nav = useNavigate();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
return <div className="row">
|
||||
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4>
|
||||
<h4 className="col-auto" style={{margin: "auto 0"}}>{t('sélectionneLesModesDaffichage')}</h4>
|
||||
<div className="col">
|
||||
{perm === "ADMIN" && <>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button>
|
||||
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>{t('secrétariatsDeLice')}</button>
|
||||
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>{t('administration')}</button>
|
||||
</>}
|
||||
{perm === "TABLE" && <>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button>
|
||||
<button className="btn btn-primary" onClick={() => nav("table")}>{t('secrétariatsDeLice')}</button>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import {usePubAffState} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx";
|
||||
import {useMemo, useRef} from 'react';
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -17,6 +18,7 @@ export function PubAffWindow({document}) {
|
||||
const chronoText = useRef(null)
|
||||
const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"})
|
||||
const state = usePubAffState();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
document.title = "Affichage Public";
|
||||
document.body.className = "bg-dark text-white overflow-hidden";
|
||||
@ -55,24 +57,24 @@ export function PubAffWindow({document}) {
|
||||
<div className="row" style={noMP}>
|
||||
<div className="col" style={noMP}>
|
||||
<CombDisplay combId={state?.c1} background={"red"}>
|
||||
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>Actuel</span>
|
||||
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>{t('actuel')}</span>
|
||||
</CombDisplay>
|
||||
</div>
|
||||
<div className="col" style={noMP}>
|
||||
<CombDisplay combId={state?.c2} background={"blue"}>
|
||||
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>contre</span>
|
||||
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>{t('contre')}</span>
|
||||
</CombDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row" style={noMP}>
|
||||
<div className="col" style={noMP}>
|
||||
<CombDisplay combId={state?.next?.[0]?.c1} background={"red"}>
|
||||
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>Suivant</span>
|
||||
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>{t('suivant')}</span>
|
||||
</CombDisplay>
|
||||
</div>
|
||||
<div className="col" style={noMP}>
|
||||
<CombDisplay combId={state?.next?.[0]?.c2} background={"blue"}>
|
||||
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>contre</span>
|
||||
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>{t('contre')}</span>
|
||||
</CombDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {CatList, getCatName} from "../../../utils/Tools.js";
|
||||
import {CombName} from "../../../hooks/useComb.jsx";
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function SelectReducer(state, action) {
|
||||
switch (action.type) {
|
||||
@ -54,6 +55,7 @@ function SelectReducer(state, action) {
|
||||
|
||||
export function SelectCombModalContent({data, setGroups}) {
|
||||
const country = useCountries('fr')
|
||||
const {t} = useTranslation("cm");
|
||||
const {dispatch} = useWS()
|
||||
const [dispo, dispoReducer] = useReducer(SelectReducer, {})
|
||||
const [select, selectReducer] = useReducer(SelectReducer, {})
|
||||
@ -153,20 +155,20 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">Sélectionner des combatants</h1>
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{t('select.sélectionnerDesCombatants')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div className="col-md-5">
|
||||
<label htmlFor="input4" className="form-label">Recherche</label>
|
||||
<label htmlFor="input4" className="form-label">{t('select.recherche')}</label>
|
||||
<input type="text" className="form-control" id="input4" value={search} onChange={(e) => setSearch(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div style={{width: "12em"}}>
|
||||
<label htmlFor="inputState0" className="form-label">Pays</label>
|
||||
<label htmlFor="inputState0" className="form-label">{t('pays')}</label>
|
||||
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
|
||||
<option value={""}>-- Tous --</option>
|
||||
<option value={""}>{t('--Tous--')}</option>
|
||||
{country && Object.keys(country).sort((a, b) => {
|
||||
if (a < b) return -1
|
||||
if (a > b) return 1
|
||||
@ -178,9 +180,9 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="inputState1" className="form-label">Club</label>
|
||||
<label htmlFor="inputState1" className="form-label">{t('club')}</label>
|
||||
<select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
|
||||
<option value={""}>-- Tous --</option>
|
||||
<option value={""}>{t('--Tous--')}</option>
|
||||
{clubList.sort((a, b) => a.localeCompare(b)).map((club) => (
|
||||
<option key={club} value={club}>{club}</option>))}
|
||||
</select>
|
||||
@ -189,50 +191,50 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div>
|
||||
<label className="form-label">Genre</label>
|
||||
<label className="form-label">{t('genre')}</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, H: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck">H</label>
|
||||
<label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
|
||||
</div>
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, F: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck2">F</label>
|
||||
<label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, NA: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck3">NA</label>
|
||||
<label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="inputState2" className="form-label">Catégorie</label>
|
||||
<label htmlFor="inputState2" className="form-label">{t('catégorie')}</label>
|
||||
<select id="inputState2" className="form-select" value={cat} onChange={(e) => setCat(Number(e.target.value))}>
|
||||
<option value={-1}>-- Tous --</option>
|
||||
<option value={-1}>{t('--Tous--')}</option>
|
||||
{CatList.map((cat, index) => {
|
||||
return (<option key={index} value={index}>{getCatName(cat)}</option>)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="input5" className="form-label">Poids</label>
|
||||
<label htmlFor="input5" className="form-label">{t('poids')}</label>
|
||||
<div className="row-cols-sm-auto d-flex align-items-center">
|
||||
<div style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0"
|
||||
name="999"
|
||||
onChange={e => setWeightMin(Number(e.target.value))}/></div>
|
||||
<div><span>à</span></div>
|
||||
<div><span>{t('select.à')}</span></div>
|
||||
<div style={{width: "4.25em"}}><input type="number" className="form-control" value={weightMax} min="0" name="999"
|
||||
onChange={e => setWeightMax(Number(e.target.value))}/></div>
|
||||
<div><small>(0 = désactivé)</small></div>
|
||||
<div><small>{t('select.msg1')}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<button style={{display: "none"}} onClick={event => event.preventDefault()}></button>
|
||||
@ -240,9 +242,9 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
<hr/>
|
||||
<div className="row g-3">
|
||||
<div className="col-md">
|
||||
<div style={{textAlign: "center"}}>Inscrit</div>
|
||||
<div style={{textAlign: "center"}}>{t('inscrit')}</div>
|
||||
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
{dispoFiltered && Object.keys(dispoFiltered).length === 0 && <div>Aucun combattant disponible</div>}
|
||||
{dispoFiltered && Object.keys(dispoFiltered).length === 0 && <div>{t('select.aucunCombattantDisponible')}</div>}
|
||||
{Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
|
||||
<button key={id} type="button" className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "")}
|
||||
onClick={() => dispoReducer({type: 'TOGGLE_ID', payload: id})}>
|
||||
@ -259,9 +261,9 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md">
|
||||
<div style={{textAlign: "center"}}>Sélectionner</div>
|
||||
<div style={{textAlign: "center"}}>{t('sélectionner')}</div>
|
||||
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
{selectFiltered && Object.keys(selectFiltered).length === 0 && <div>Aucun combattant sélectionné</div>}
|
||||
{selectFiltered && Object.keys(selectFiltered).length === 0 && <div>{t('select.aucunCombattantSélectionné')}</div>}
|
||||
{Object.keys(selectFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
|
||||
<button key={id} type="button"
|
||||
className={"list-group-item list-group-item-action " + (selectFiltered[id] ? "active" : "")}
|
||||
@ -273,15 +275,15 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<div className="vr"></div>
|
||||
<label htmlFor="input6" className="form-label">Poule</label>
|
||||
<label htmlFor="input6" className="form-label">{t('poule')}</label>
|
||||
<input type="text" className="form-control" id="input6" style={{width: "3em"}} maxLength={1} value={targetGroupe}
|
||||
onChange={(e) => {
|
||||
if (/^[a-zA-Z0-9]$/.test(e.target.value))
|
||||
setTargetGroupe(e.target.value)
|
||||
}}/>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>Ajouter</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>{t('ajouter')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ 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 {useAuth} from "../../hooks/useAuth.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ResultList() {
|
||||
const navigate = useNavigate();
|
||||
@ -25,14 +25,15 @@ export function ResultList() {
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, navigate}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<div className="mb-4">
|
||||
<h4>Compétition:</h4>
|
||||
<h4>{t('competition', {count: data.length})}:</h4>
|
||||
<div className="list-group">
|
||||
{data.sort((a, b) => new Date(b[2].split('T')[0]) - new Date(a[2].split('T')[0])).map((o) => (
|
||||
<li className="list-group-item list-group-item-action" key={o[0]}
|
||||
onClick={e => navigate(`${o[0]}`)}>{o[1]}</li>))}
|
||||
onClick={() => navigate(`${o[0]}`)}>{o[1]}</li>))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -2,10 +2,13 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||
import {Outlet} from "react-router-dom";
|
||||
import {ResultList} from "./ResultList.jsx";
|
||||
import {ResultView} from "./ResultView.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ResultRoot() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
<h1>Résultat</h1>
|
||||
<h1>{t("result", {count: 1})}</h1>
|
||||
<LoadingProvider>
|
||||
<Outlet/>
|
||||
</LoadingProvider>
|
||||
|
||||
@ -7,6 +7,7 @@ import React, {useEffect, useState} from "react";
|
||||
import {DrawGraph} from "./DrawGraph.jsx";
|
||||
import {TreeNode} from "../../utils/TreeUtils.js";
|
||||
import {scoreToString} from "../../utils/CompetitionTools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
function CupImg() {
|
||||
return <img decoding="async" loading="lazy" width="16" height="16" className="wp-image-1635"
|
||||
@ -18,10 +19,11 @@ export function ResultView() {
|
||||
const {uuid} = useParams()
|
||||
const navigate = useNavigate();
|
||||
const [resultShow, setResultShow] = useState("cat")
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
return <>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/result")}>
|
||||
« retour
|
||||
{t('back')}
|
||||
</button>
|
||||
|
||||
<MenuBar resultShow={resultShow} setResultShow={setResultShow}/>
|
||||
@ -40,22 +42,23 @@ export function ResultView() {
|
||||
// || resultShow && resultShow === "club_all" && <ClubAllResult uuid={uuid}/>
|
||||
|
||||
function MenuBar({resultShow, setResultShow}) {
|
||||
const {t} = useTranslation('result');
|
||||
return <ul className="nav nav-tabs">
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link my-1" + (resultShow === "cat" ? " active" : "")} aria-current={(resultShow === "cat" ? " page" : "false")}
|
||||
href="#" onClick={_ => setResultShow("cat")}>Par catégorie</a>
|
||||
href="#" onClick={_ => setResultShow("cat")}>{t('parCatégorie')}</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link my-1" + (resultShow === "club" ? " active" : "")} aria-current={(resultShow === "club" ? " page" : "false")}
|
||||
href="#" onClick={_ => setResultShow("club")}>Par club</a>
|
||||
href="#" onClick={_ => setResultShow("club")}>{t('parClub')}</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link my-1" + (resultShow === "comb" ? " active" : "")} aria-current={(resultShow === "comb" ? " page" : "false")}
|
||||
href="#" onClick={_ => setResultShow("comb")}>Par combattant</a>
|
||||
href="#" onClick={_ => setResultShow("comb")}>{t('parCombattant')}</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")}
|
||||
href="#" onClick={_ => setResultShow("combs")}>Combattants</a>
|
||||
href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -69,15 +72,16 @@ function MenuBar({resultShow, setResultShow}) {
|
||||
}
|
||||
|
||||
function BuildMatchArray({matchs}) {
|
||||
const {t} = useTranslation('result');
|
||||
return <>
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style={{textAlign: "center"}}>Rouge</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('rouge')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}></th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Scores</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('scores')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}></th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Bleu</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('bleu')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -94,16 +98,17 @@ function BuildMatchArray({matchs}) {
|
||||
}
|
||||
|
||||
function BuildRankArray({rankArray}) {
|
||||
const {t} = useTranslation('result');
|
||||
return <>
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style={{textAlign: "center"}}>Place</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Nom</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Victoire</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Ratio</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Points marqués</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Points reçus</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('place')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('nom')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('victoire')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('ratio')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('pointsMarqués')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('pointsReçus')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -147,6 +152,7 @@ function CategoryList({uuid}) {
|
||||
const [catId, setCatId] = useState(null)
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/result/${uuid}/category/list`, setLoading, 1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
useEffect(() => {
|
||||
if (data && Object.keys(data).length > 0)
|
||||
@ -155,7 +161,7 @@ function CategoryList({uuid}) {
|
||||
|
||||
return <>
|
||||
{data ? <div className="input-group" style={{marginBottom: "1em"}}>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>{t('catégorie')}</h6>
|
||||
<select className="form-select" aria-label="Select Result Type" onChange={e => setCatId(e.target.value)}>
|
||||
{Object.keys(data).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))
|
||||
.map(key => <option key={key} value={data[key]}>{key}</option>)}
|
||||
@ -170,6 +176,7 @@ function CategoryList({uuid}) {
|
||||
|
||||
function CategoryResult({uuid, catId}) {
|
||||
const [type, setType] = useState(1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/result/${uuid}/category/${catId}`, setLoading, 1)
|
||||
@ -194,18 +201,18 @@ function CategoryResult({uuid, catId}) {
|
||||
<ul className="nav nav-tabs">
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")} href="#"
|
||||
onClick={_ => setType(1)}>Poule</a>
|
||||
onClick={_ => setType(1)}>{t('poule')}</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} href="#"
|
||||
onClick={_ => setType(2)}>Tournois</a>
|
||||
onClick={_ => setType(2)}>{t('tournois')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</>}
|
||||
|
||||
{type === 1 && <>
|
||||
{Object.keys(data.matchs).map(p => <div key={p}>
|
||||
{Object.keys(data.matchs).length > 1 && <h4 style={{marginTop: "2em"}}>Poule {p}</h4>}
|
||||
{Object.keys(data.matchs).length > 1 && <h4 style={{marginTop: "2em"}}>{t('poule')} {p}</h4>}
|
||||
<BuildMatchArray matchs={data.matchs[p]}/>
|
||||
<BuildRankArray rankArray={data.rankArray[p]}/>
|
||||
</div>)}
|
||||
@ -222,6 +229,7 @@ function ClubList({uuid}) {
|
||||
const [clubId, setClubId] = useState(null)
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/result/${uuid}/club/list`, setLoading, 1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
useEffect(() => {
|
||||
if (data && Object.keys(data).length > 0)
|
||||
@ -230,7 +238,7 @@ function ClubList({uuid}) {
|
||||
|
||||
return <>
|
||||
{data ? <div className="input-group" style={{marginBottom: "1em"}}>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>Club</h6>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>{t('club')}</h6>
|
||||
<select className="form-select" aria-label="Select Result Type" onChange={e => setClubId(e.target.value)}>
|
||||
{Object.keys(data).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))
|
||||
.map(key => <option key={key} value={data[key]}>{key}</option>)}
|
||||
@ -246,6 +254,7 @@ function ClubList({uuid}) {
|
||||
function ClubResult({uuid, clubId}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/result/${uuid}/club/${clubId}`, setLoading, 1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
useEffect(() => {
|
||||
refresh(`/result/${uuid}/club/${clubId}`)
|
||||
@ -253,34 +262,34 @@ function ClubResult({uuid, clubId}) {
|
||||
|
||||
return <>
|
||||
{data ? <>
|
||||
<h3>Info :</h3>
|
||||
<h3>{t('info')} :</h3>
|
||||
<ul>
|
||||
<li>Nom : {data.name}</li>
|
||||
<li>Nombre d'inscris : {data.nb_insc}</li>
|
||||
<li>{t('nom')} : {data.name}</li>
|
||||
<li>{t('nombreDinscris')} : {data.nb_insc}</li>
|
||||
</ul>
|
||||
<h3>Statistique :</h3>
|
||||
<h3>{t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>Nombre de match disputé : {data.nb_match}</li>
|
||||
<li>Nombre de victoires : {data.match_w}</li>
|
||||
<li>Ratio de victoires moyen : {data.ratioVictoire.toFixed(3)}</li>
|
||||
<li>Points marqués : {data.pointMake}</li>
|
||||
<li>Points reçus : {data.pointTake}</li>
|
||||
<li>Ratio de points moyen : {data.ratioPoint.toFixed(3)}</li>
|
||||
<li>{t('nombreDeMatchDisputé2', {nb: data.nb_match})}</li>
|
||||
<li>{t('nombreDeVictoires2', {nb: data.match_w})}</li>
|
||||
<li>{t('ratioDeVictoiresMoyen2', {nb: data.ratioVictoire.toFixed(3)})}</li>
|
||||
<li>{t('pointsMarqués2', {nb: data.pointMake})}</li>
|
||||
<li>{t('pointsReçus2', {nb: data.pointTake})}</li>
|
||||
<li>{t('ratioDePointsMoyen2', {nb: data.ratioPoint.toFixed(3)})}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des membres :</h3>
|
||||
<h3>{t('listeDesMembres')} :</h3>
|
||||
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style={{textAlign: "center"}}>Catégorie</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Nom</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Victoires</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Défaites</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Ratio victoires</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Points marqués</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Points reçus</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Ratio points</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('catégorie')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('nom')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('victoires')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('défaites')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('ratioVictoires')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('pointsMarqués')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('pointsReçus')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('ratioPoints')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -309,6 +318,7 @@ function CombList({uuid}) {
|
||||
const [combId, setCombId] = useState(null)
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/result/${uuid}/comb/list`, setLoading, 1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
useEffect(() => {
|
||||
if (data && Object.keys(data).length > 0)
|
||||
@ -317,7 +327,7 @@ function CombList({uuid}) {
|
||||
|
||||
return <>
|
||||
{data ? <div className="input-group" style={{marginBottom: "1em"}}>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>Combattant</h6>
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>{t('combattant')}</h6>
|
||||
<select className="form-select" aria-label="Select Result Type" onChange={e => setCombId(e.target.value)}>
|
||||
{Object.keys(data).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))
|
||||
.map(key => <option key={key} value={data[key]}>{key}</option>)}
|
||||
@ -333,6 +343,7 @@ function CombList({uuid}) {
|
||||
function CombResult({uuid, combId}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/result/${uuid}/comb/${combId}`, setLoading, 1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
useEffect(() => {
|
||||
refresh(`/result/${uuid}/comb/${combId}`)
|
||||
@ -346,31 +357,34 @@ function CombResult({uuid, combId}) {
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>Info :</h3>
|
||||
<h3>{t('info')} :</h3>
|
||||
<ul>
|
||||
<li>Nom Prénom : {data.name}</li>
|
||||
<li>Club : {data.club}</li>
|
||||
<li>Catégorie : {data.cat}</li>
|
||||
<li>{t('nomPrénom')} : {data.name}</li>
|
||||
<li>{t('club')} : {data.club}</li>
|
||||
<li>{t('catégorie')} : {data.cat}</li>
|
||||
</ul>
|
||||
<h3>Statistique :</h3>
|
||||
<h3>{t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>Taux de victoire : {data.matchs.length === 0 ? "---" : (data.totalWin / data.matchs.length * 100).toFixed(0)}% ({data.totalWin} sur
|
||||
${data.matchs.length})
|
||||
<li>{t('tauxDeVictoire2', {
|
||||
nb: data.matchs.length === 0 ? "---" : (data.totalWin / data.matchs.length * 100).toFixed(0),
|
||||
victoires: data.totalWin,
|
||||
matchs: data.matchs.length
|
||||
})}
|
||||
</li>
|
||||
<li>Points marqués : {data.pointMake}</li>
|
||||
<li>Points reçus : {data.pointTake}</li>
|
||||
<li>Ratio du score (point marqué / point reçu): {data.pointRatio.toFixed(3)}</li>
|
||||
<li>{t('pointsMarqués2', {nb: data.pointMake})}</li>
|
||||
<li>{t('pointsReçus2', {nb: data.pointTake})}</li>
|
||||
<li>{t('ratioDuScore2', {nb: data.pointRatio.toFixed(3)})}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des matchs:</h3>
|
||||
<h3>{t('listeDesMatchs')}:</h3>
|
||||
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style={{textAlign: "center"}}>Catégorie</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Adversaire</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Scores</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Ratio</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('catégorie')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('adversaire')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t("scores")}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('ratio')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -391,30 +405,31 @@ function CombResult({uuid, combId}) {
|
||||
function CombsResult({uuid}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1)
|
||||
const {t} = useTranslation('result');
|
||||
|
||||
return <>
|
||||
{data ? <>
|
||||
<h3>Statistique :</h3>
|
||||
<h3>{t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>Nombre d'inscris : {data.nb_insc}</li>
|
||||
<li>Nombre de match disputé : {data.tt_match}</li>
|
||||
<li>Points marqués : {data.point}</li>
|
||||
<li>{t('nombreDinscris2', {nb: data.nb_insc})}</li>
|
||||
<li>{t('nombreDeMatchDisputé2', {nb: data.tt_match})}</li>
|
||||
<li>{t('pointsMarqués2', {nb: data.point})}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des combattants :</h3>
|
||||
<h3>{t('listeDesCombattants')} :</h3>
|
||||
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style={{textAlign: "center"}}>Catégorie</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Club</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Nom</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Victoires</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Défaites</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Ratio victoires</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Points marqués</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Points reçus</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>Ratios points</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('catégorie')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('club')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('nom')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('victoires')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('défaites')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('ratioVictoires')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('pointsMarqués')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('pointsReçus')}</th>
|
||||
<th scope="col" style={{textAlign: "center"}}>{t('ratiosPoints')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import i18n from "../config/i18n.js";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -78,34 +79,46 @@ export function getSaison(currentDate = new Date()) {
|
||||
export function getCatName(cat) {
|
||||
switch (cat) {
|
||||
case "SUPER_MINI":
|
||||
return "Super Mini";
|
||||
return i18n.t('cat.superMini');
|
||||
case "MINI_POUSSIN":
|
||||
return "Mini Poussin";
|
||||
return i18n.t('cat.miniPoussin');
|
||||
case "POUSSIN":
|
||||
return "Poussin";
|
||||
return i18n.t('cat.poussin');
|
||||
case "BENJAMIN":
|
||||
return "Benjamin";
|
||||
return i18n.t('cat.benjamin');
|
||||
case "MINIME":
|
||||
return "Minime";
|
||||
return i18n.t('cat.minime');
|
||||
case "CADET":
|
||||
return "Cadet";
|
||||
return i18n.t('cat.cadet');
|
||||
case "JUNIOR":
|
||||
return "Junior";
|
||||
return i18n.t('cat.junior');
|
||||
case "SENIOR1":
|
||||
return "Senior 1";
|
||||
return i18n.t('cat.senior1');
|
||||
case "SENIOR2":
|
||||
return "Senior 2";
|
||||
return i18n.t('cat.senior2');
|
||||
case "VETERAN1":
|
||||
return "Vétéran 1";
|
||||
return i18n.t('cat.vétéran1');
|
||||
case "VETERAN2":
|
||||
return "Vétéran 2";
|
||||
return i18n.t('cat.vétéran2');
|
||||
case null:
|
||||
return "Catégorie inconnue";
|
||||
return i18n.t('cat.catégorieInconnue');
|
||||
default:
|
||||
return cat;
|
||||
}
|
||||
}
|
||||
|
||||
export function getToastMessage(msgKey) {
|
||||
return {
|
||||
pending: i18n.t(msgKey + '.pending'),
|
||||
success: i18n.t(msgKey + '.success'),
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, i18n.t(msgKey + '.error'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function win(scores) {
|
||||
let sum = 0
|
||||
for (const score of scores) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user