dev #105

Merged
Thibaut merged 8 commits from dev into master 2026-01-23 15:49:25 +00:00
22 changed files with 941 additions and 180 deletions
Showing only changes of commit a5d3973394 - Show all commits

View File

@ -0,0 +1,71 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "category_preset")
public class CatPresetModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "competition", referencedColumnName = "id")
CompetitionModel competition;
String name = "";
List<Categorie> categories;
long roundDuration;
long pauseDuration;
SwordType swordType = SwordType.NONE;
ShieldType shieldType = ShieldType.NONE;
/* Bitmask protections:
* 1 - 1 - Head
* 2 - 2 - Throat
* 3 - 4 - Torso
* 4 - 8 - Arms
* 5 - 16 - Hands
* 6 - 32 - Groin
* 7 - 64 - Legs
*/
int mandatoryProtection = 0;
@ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY)
private List<RegisterModel> registers = new ArrayList<>();
@ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY)
private List<CompetitionGuestModel> guest = new ArrayList<>();
public enum SwordType {
NONE,
ONE_HAND,
TWO_HAND,
SABER
}
public enum ShieldType {
NONE,
STANDARD,
ROUND,
TEARDROP,
BUCKLER
}
}

View File

@ -44,4 +44,8 @@ public class CategoryModel {
Integer type;
String liceName = "1";
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "id_preset", referencedColumnName = "id")
CatPresetModel preset;
}

View File

@ -61,6 +61,14 @@ public class CompetitionGuestModel implements CombModel {
)
List<CompetitionGuestModel> guest = new ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(
name = "categories_insc_guest",
joinColumns = @JoinColumn(name = "guest_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
public CompetitionGuestModel(String s) {
this.fname = s.substring(0, s.indexOf(" "));
this.lname = s.substring(s.indexOf(" ") + 1);

View File

@ -58,6 +58,8 @@ public class CompetitionModel {
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<CompetitionGuestModel> guests = new ArrayList<>();
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
List<CatPresetModel> catPreset = new ArrayList<>();
List<Long> banMembre = new ArrayList<>();

View File

@ -11,6 +11,9 @@ import lombok.Setter;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@ -46,6 +49,17 @@ public class RegisterModel {
@Column(nullable = false, columnDefinition = "boolean default false")
boolean lockEdit = false;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(
name = "categories_insc_comb",
joinColumns = {
@JoinColumn(name = "id_competition", referencedColumnName = "id_competition"),
@JoinColumn(name = "id_membre", referencedColumnName = "id_membre")
},
inverseJoinColumns = @JoinColumn(name = "category_id")
)
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
public RegisterModel(CompetitionModel competition, MembreModel membre, Integer weight, int overCategory,
Categorie categorie, ClubModel club) {
this.id = new RegisterId(competition.getId(), membre.getId());

View File

@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CatPresetModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CatPresetRepository implements PanacheRepositoryBase<CatPresetModel, Long> {
}

View File

@ -98,12 +98,14 @@ public class CompetPermService {
map.putIfAbsent(model.getId(), "owner");
else if (securityCtx.roleHas("federation_admin"))
map.putIfAbsent(model.getId(), "admin");
else if (securityCtx.isInClubGroup(model.getClub().getId()) && (securityCtx.roleHas(
"club_president")
|| securityCtx.roleHas("club_respo_intra") || securityCtx.roleHas(
"club_secretaire")
|| securityCtx.roleHas("club_tresorier")))
else if (securityCtx.isInClubGroup(
model.getClub().getId()) && (securityCtx.isClubAdmin()))
map.putIfAbsent(model.getId(), "admin");
else if (model.getAdmin().contains(securityCtx.getSubject()))
map.putIfAbsent(model.getId(), "admin");
else if (model.getTable().contains(securityCtx.getSubject()))
map.putIfAbsent(model.getId(), "table");
}
return map;
}));
@ -182,12 +184,14 @@ public class CompetPermService {
if (o.getSystem() == CompetitionSystem.SAFCA)
return hasSafcaViewPerm(securityCtx, o.getId());
if (o.getAdmin().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here
throw new DForbiddenException();
if (o.getSystem() == CompetitionSystem.INTERNAL)
if (securityCtx.roleHas("club_president") || securityCtx.roleHas("club_respo_intra")
|| securityCtx.roleHas("club_secretaire") || securityCtx.roleHas("club_tresorier"))
if (securityCtx.isClubAdmin())
return Uni.createFrom().nullItem();
throw new DForbiddenException();

View File

@ -7,10 +7,7 @@ import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import fr.titionfire.ffsaf.net2.request.SReqCompet;
import fr.titionfire.ffsaf.net2.request.SReqRegister;
import fr.titionfire.ffsaf.rest.client.dto.NotificationData;
import fr.titionfire.ffsaf.rest.data.CompetitionData;
import fr.titionfire.ffsaf.rest.data.RegisterRequestData;
import fr.titionfire.ffsaf.rest.data.SimpleCompetData;
import fr.titionfire.ffsaf.rest.data.SimpleRegisterComb;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
@ -63,6 +60,9 @@ public class CompetitionService {
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CatPresetRepository catPresetRepository;
@Inject
ServerCustom serverCustom;
@ -112,16 +112,14 @@ public class CompetitionService {
public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
if (id == 0) {
return Uni.createFrom()
.item(new CompetitionData(null, "", "", "", "", new Date(), new Date(),
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
null, "", "", null, true, true,
"", "", "", "", "{}"));
return Uni.createFrom().item(new CompetitionData());
}
return permService.hasAdminViewPerm(securityCtx, id)
.call(competitionModel -> Mutiny.fetch(competitionModel.getCatPreset()))
.chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
.chain(insc -> Mutiny.fetch(competitionModel.getGuests())
.map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest))))
.map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest)
.addPresets(competitionModel.getCatPreset()))))
.chain(data ->
vertx.getOrCreateContext().executeBlocking(() -> {
keycloakService.getUser(UUID.fromString(data.getOwner()))
@ -208,17 +206,21 @@ public class CompetitionService {
model.setGuests(new ArrayList<>());
model.setUuid(UUID.randomUUID().toString());
model.setOwner(securityCtx.getSubject());
model.setCatPreset(new ArrayList<>());
copyData(data, model);
return Panache.withTransaction(() -> repository.persist(model));
}).map(CompetitionData::fromModel)
})
.call(model -> syncPreset(data, model))
.map(CompetitionData::fromModel)
.call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem())
.call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem());
} else {
return permService.hasEditPerm(securityCtx, data.getId())
.call(model -> Mutiny.fetch(model.getCatPreset()))
.chain(model -> {
copyData(data, model);
@ -237,7 +239,9 @@ public class CompetitionService {
}
}))
.chain(__ -> Panache.withTransaction(() -> repository.persist(model)));
}).map(CompetitionData::fromModel)
})
.call(model -> syncPreset(data, model))
.map(model -> CompetitionData.fromModel(model).addPresets(model.getCatPreset()))
.call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
securityCtx.getSubject()) : Uni.createFrom().nullItem())
.call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
@ -245,6 +249,46 @@ public class CompetitionService {
}
}
private Uni<?> syncPreset(CompetitionData data, CompetitionModel model) {
List<Long> toRemoveId = model.getCatPreset().stream()
.map(CatPresetModel::getId)
.filter(id -> data.getPresets().stream().noneMatch(preset -> Objects.equals(preset.getId(), id)))
.toList();
for (PresetData preset : data.getPresets()) {
CatPresetModel presetModel;
if (preset.getId() != null && preset.getId() > 0) {
presetModel = model.getCatPreset().stream()
.filter(p -> p.getId().equals(preset.getId()))
.findFirst()
.orElse(new CatPresetModel());
} else {
presetModel = new CatPresetModel();
model.getCatPreset().add(presetModel);
}
presetModel.setCompetition(model);
presetModel.setName(preset.getName());
presetModel.setSwordType(preset.getSword());
presetModel.setShieldType(preset.getShield());
presetModel.setRoundDuration(preset.getRoundDuration());
presetModel.setPauseDuration(preset.getPauseDuration());
presetModel.setCategories(preset.getCategories());
presetModel.setMandatoryProtection(preset.getMandatoryProtection());
}
// Remove deleted presets
model.getCatPreset().removeIf(presetModel -> toRemoveId.contains(presetModel.getId()));
return Panache.withTransaction(() -> repository.persist(model)
.call(__ -> {
if (!toRemoveId.isEmpty()) {
return catPresetRepository.delete("id IN ?1", toRemoveId);
}
return Uni.createFrom().nullItem();
}));
}
private void copyData(CompetitionData data, CompetitionModel model) {
if (model.getBanMembre() == null)
model.setBanMembre(new ArrayList<>());
@ -271,11 +315,17 @@ public class CompetitionService {
Uni<List<SimpleRegisterComb>> uni = Mutiny.fetch(c.getInsc())
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
.map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences()))
.onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
.map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences())
.setCategorieInscrite(cm.getCategoriesInscrites()))
.collect().asList();
return uni
.call(l -> Mutiny.fetch(c.getGuests())
.map(guest -> guest.stream().map(SimpleRegisterComb::fromModel).toList())
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(guest -> Mutiny.fetch(guest.getCategoriesInscrites()))
.map(guest -> SimpleRegisterComb.fromModel(guest)
.setCategorieInscrite(guest.getCategoriesInscrites()))
.collect().asList()
.invoke(l::addAll));
});
@ -290,11 +340,15 @@ public class CompetitionService {
model.getClub()))
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
.map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences()))
.onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
.map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())
.setCategorieInscrite(combModel.getCategoriesInscrites()))
.collect().asList();
return membreService.getByAccountId(securityCtx.getSubject())
.chain(model -> registerRepository.find("competition.id = ?1 AND membre = ?2", id, model).firstResult()
.call(rm -> rm == null ? Uni.createFrom().voidItem() :
Mutiny.fetch(rm.getCategoriesInscrites()))
.map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of()))));
}
@ -312,7 +366,8 @@ public class CompetitionService {
return Panache.withTransaction(() -> repository.persist(c));
})
.chain(combModel -> updateRegister(data, c, combModel, true)))
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()));
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites()));
} else {
return permService.hasEditPerm(securityCtx, id)
.chain(c -> competitionGuestRepository.findById(data.getId() * -1)
@ -323,7 +378,7 @@ public class CompetitionService {
model.setCompetition(c);
return model;
}))
.chain(model -> {
.invoke(model -> {
model.setFname(data.getFname());
if (data.getLname().equals("__team"))
model.setLname("_team");
@ -334,13 +389,21 @@ public class CompetitionService {
model.setCountry(data.getCountry());
model.setWeight(data.getWeight());
model.setCategorie(data.getCategorie());
return Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(model.getCompetition().getUuid(),
r) : Uni.createFrom().voidItem());
})
.map(SimpleRegisterComb::fromModel);
.call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(),
data.getCategoriesInscrites())
.invoke(cats -> {
g.getCategoriesInscrites().clear();
g.getCategoriesInscrites().addAll(cats);
g.getCategoriesInscrites()
.removeIf(cat -> !cat.getCategories().contains(g.getCategorie()));
}))
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(model.getCompetition().getUuid(),
r) : Uni.createFrom().voidItem()))
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
}
if ("club".equals(source))
return repository.findById(id)
@ -418,6 +481,19 @@ public class CompetitionService {
}
return r;
}))
.call(r -> Mutiny.fetch(r.getCategoriesInscrites()).chain(__ ->
catPresetRepository.list("competition = ?1 AND id IN ?2", c, data.getCategoriesInscrites())
.invoke(cats -> {
if (data.isQuick()) {
cats.removeIf(cat -> r.getCategoriesInscrites().stream()
.anyMatch(cp -> cp.equals(cat)));
} else {
r.getCategoriesInscrites().clear();
}
r.getCategoriesInscrites().addAll(cats);
r.getCategoriesInscrites()
.removeIf(cat -> !cat.getCategories().contains(r.getCategorie2()));
})))
.chain(r -> Panache.withTransaction(() -> registerRepository.persist(r)))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem());
@ -660,6 +736,12 @@ public class CompetitionService {
.call(__ -> cache.invalidate(data.getId()));
}
public Uni<List<PresetData>> getPresetsForCompetition(SecurityCtx securityCtx, Long id) {
return permService.hasViewPerm(securityCtx, id)
.chain(cm -> Mutiny.fetch(cm.getCatPreset()))
.map(p -> p.stream().map(PresetData::fromModel).toList());
}
public Uni<Response> unregisterHelloAsso(NotificationData data) {
if (!data.getState().equals("Refunded"))
return Uni.createFrom().item(Response.ok().build());
@ -693,8 +775,8 @@ public class CompetitionService {
public Uni<Response> registerHelloAsso(NotificationData data) {
String organizationSlug = data.getOrganizationSlug();
String formSlug = data.getFormSlug();
RegisterRequestData req = new RegisterRequestData(null, "", "", null, 0, false, null, Categorie.CADET, Genre.NA,
null, "fr");
RegisterRequestData req = new RegisterRequestData(null, "", "", null, 0, false, new ArrayList<>(), null,
Categorie.CADET, Genre.NA, null, "fr", false);
return repository.find("data1 = ?1 AND data2 = ?2", organizationSlug, formSlug).firstResult()
.onFailure().recoverWithNull()

View File

@ -1,10 +1,7 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.CompetitionService;
import fr.titionfire.ffsaf.rest.data.CompetitionData;
import fr.titionfire.ffsaf.rest.data.RegisterRequestData;
import fr.titionfire.ffsaf.rest.data.SimpleCompetData;
import fr.titionfire.ffsaf.rest.data.SimpleRegisterComb;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
@ -79,6 +76,13 @@ public class CompetitionEndpoints {
return service.getInternalData(securityCtx, id);
}
@GET
@Path("{id}/categories")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<PresetData>> getPresetsForCompetition(@PathParam("id") Long id) {
return service.getPresetsForCompetition(securityCtx, id);
}
@GET
@Path("all")

View File

@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.CatPresetModel;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import fr.titionfire.ffsaf.data.model.RegisterModel;
@ -10,6 +11,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;
@ -41,6 +43,14 @@ public class CompetitionData {
private String data3;
private String data4;
private String config;
private List<PresetData> presets;
public CompetitionData () {
this(null, "", "", "", "", new Date(), new Date(),
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
null, "", "", null, true, true,
"", "", "", "", "{}", new ArrayList<>());
}
public static CompetitionData fromModel(CompetitionModel model) {
if (model == null)
@ -50,7 +60,7 @@ public class CompetitionData {
model.getUuid(), model.getDate(), model.getTodate(), model.getSystem(),
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
model.getClub().getId(), model.getClub().getName(), model.getOwner(), null, false, false,
model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig());
model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig(), new ArrayList<>());
}
public static CompetitionData fromModelLight(CompetitionModel model) {
@ -61,7 +71,7 @@ public class CompetitionData {
model.getAdresse(), "", model.getDate(), model.getTodate(), null,
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
null, model.getClub().getName(), "", null, false, false,
"", "", "", "", "{}");
"", "", "", "", "{}", new ArrayList<>());
if (model.getRegisterMode() == RegisterMode.HELLOASSO) {
out.setData1(model.getData1());
@ -84,6 +94,11 @@ public class CompetitionData {
return this;
}
public CompetitionData addPresets(List<CatPresetModel> presets) {
this.presets = presets.stream().map(PresetData::fromModel).toList();
return this;
}
@Data
@AllArgsConstructor
@RegisterForReflection

View File

@ -0,0 +1,32 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.CatPresetModel;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
@RegisterForReflection
public class PresetData {
private Long id;
private String name;
private CatPresetModel.SwordType sword;
private CatPresetModel.ShieldType shield;
private List<Categorie> categories;
long roundDuration;
long pauseDuration;
int mandatoryProtection;
public static PresetData fromModel(CatPresetModel model) {
if (model == null)
return null;
return new PresetData(model.getId(), model.getName(), model.getSwordType(), model.getShieldType(),
model.getCategories(), model.getRoundDuration(), model.getPauseDuration(),
model.getMandatoryProtection());
}
}

View File

@ -7,6 +7,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ -19,6 +21,7 @@ public class RegisterRequestData {
private Integer weight;
private int overCategory;
private boolean lockEdit = false;
private List<Long> categoriesInscrites;
// for guest registration only
private Long id = null;
@ -26,4 +29,6 @@ public class RegisterRequestData {
private Genre genre = Genre.NA;
private String club = null;
private String country = null;
private boolean quick = false;
}

View File

@ -1,9 +1,6 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.model.RegisterModel;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
@ -12,6 +9,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@ -30,6 +28,7 @@ public class SimpleRegisterComb {
private int overCategory;
private boolean hasLicenceActive;
private boolean lockEdit;
private List<Long> categoriesInscrites;
public static SimpleRegisterComb fromModel(RegisterModel register, List<LicenceModel> licences) {
MembreModel membreModel = register.getMembre();
@ -39,13 +38,19 @@ public class SimpleRegisterComb {
SimpleClubModel.fromModel(register.getClub()), membreModel.getLicence(), register.getWeight(),
register.getOverCategory(),
licences.stream().anyMatch(l -> l.isValidate() && l.getSaison() == Utils.getSaison()),
register.isLockEdit());
register.isLockEdit(), new ArrayList<>());
}
public static SimpleRegisterComb fromModel(CompetitionGuestModel guest) {
return new SimpleRegisterComb(guest.getId() * -1, guest.getFname(), guest.getLname(),
guest.getGenre(), guest.getCountry(), guest.getCategorie(),
new SimpleClubModel(null, guest.getClub(), "fr", null),
null, guest.getWeight(), 0, false, false);
null, guest.getWeight(), 0, false, false,
new ArrayList<>());
}
public SimpleRegisterComb setCategorieInscrite(List<CatPresetModel> presets) {
this.categoriesInscrites = presets.stream().map(CatPresetModel::getId).toList();
return this;
}
}

View File

@ -102,6 +102,7 @@
"supprimer": "Delete",
"sélectionneLesModesDaffichage": "Select display modes",
"sélectionner": "Select",
"team": "Team",
"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",
@ -113,6 +114,9 @@
"toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.",
"toast.team.update.error": "Error while updating team",
"toast.team.update.pending": "Updating team...",
"toast.team.update.success": "Team updated!",
"toast.updateCategory.error": "Error while updating the category",
"toast.updateCategory.pending": "Updating category...",
"toast.updateCategory.success": "Category updated!",
@ -125,12 +129,8 @@
"toast.updateTrees.init.success": "Trees created!",
"toast.updateTrees.pending": "Updating tournament trees...",
"toast.updateTrees.success": "Trees updated!",
"toast.team.update.error": "Error while updating team",
"toast.team.update.pending": "Updating team...",
"toast.team.update.success": "Team updated!",
"tournoi": "Tournament",
"tournois": "Tournaments",
"team": "Team",
"tousLesMatchs": "All matches",
"toutConserver": "Keep all",
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",

View File

@ -1,6 +1,7 @@
{
"(optionnelle)": "(optional)",
"---SansClub---": "--- no club ---",
"---TousLesAges---": "--- all ages ---",
"---ToutLesClubs---": "--- all clubs ---",
"---ToutLesPays---": "--- all countries ---",
"---TouteLesCatégories---": "--- all categories ---",
@ -8,6 +9,7 @@
"--SélectionnerCatégorie--": "-- Select category --",
"1Catégorie": "+1 category",
"2Catégorie": "+2 categories",
"LesModificationsNontEnregistrer": "/!\\ The changes have not yet been saved, click save /!\\",
"activer": "Activate",
"admin": "Administration",
"administrateur": "Administrator",
@ -81,11 +83,14 @@
"ajouterUnClub": "Add a club",
"ajouterUnMembre": "Add a member",
"all_season": "--- all seasons ---",
"arme": "Weapon",
"au": "to",
"aucun": "None",
"aucunMembreSélectionné": "No member selected",
"aucuneCatégorieDisponible": "No categories available at this time.",
"back": "« back",
"blason": "Coat of arms",
"bouclier": "Shield",
"bureau": "Board",
"button.accepter": "Accept",
"button.ajouter": "Add",
@ -93,7 +98,6 @@
"button.appliquer": "Apply",
"button.confirmer": "Confirm",
"button.créer": "Create",
"button.enregister": "Save",
"button.enregistrer": "Save",
"button.fermer": "Close",
"button.modifier": "Edit",
@ -115,6 +119,7 @@
"cat.vétéran2": "Veteran 2",
"categorie": "category",
"catégorie": "Category",
"catégorieàAjouter": "Category to add",
"certificatMédical": "Medical certificate",
"chargement...": "Loading...",
"chargerLexcel": "Load Excel",
@ -249,6 +254,7 @@
"compte": "Account",
"compétition": "Competition",
"configuration": "Configuration",
"configurationDeLaCatégorie": "Category configuration",
"conserverLancienEmail": "Keep the old email",
"contactAdministratif": "Administrative contact",
"contactInterne": "Internal contact",
@ -274,6 +280,8 @@
"donnéesAdministratives": "Administrative data",
"du": "From",
"dun": "of a",
"duréePause": "Pause duration",
"duréeRound": "Round duration",
"définirLidDuCompte": "Define account ID",
"editionDeL'affiliation": "Editing affiliation",
"editionDeLaDemande": "Editing request",
@ -474,6 +482,7 @@
"permission": "Permission",
"photos": "Photos",
"prenom": "First name",
"protectionObligatoire": "Mandatory protection",
"prénomEtNom": "First and last name",
"rechercher": "Search",
"rechercher...": "Search...",
@ -494,8 +503,15 @@
"role.vise-secrétaire": "Vice-Secretary",
"role.vise-trésorier": "Vice-Treasurer",
"saison": "Season",
"sans": "Without",
"secrétariatsDeLice": "Ring secretariats",
"selectionner...": "Select...",
"shield.buckler": "Buckler",
"shield.none": "$t(sans) / $t(nonDéfinie)",
"shield.round": "Round",
"shield.standard": "Standard",
"shield.teardrop": "Teardrop",
"siDisponiblePourLaCatégorieDages": "If available for the age category",
"siretOuRna": "SIRET or RNA",
"stats": "Statistics",
"statue": "Statue",
@ -505,6 +521,10 @@
"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?",
"sword.none": "$t(sans) / $t(nonDéfinie)",
"sword.oneHand": "One hand",
"sword.saber": "Saber",
"sword.twoHand": "Two hands",
"sélectionEnéquipeDeFrance": "Selection in the French team",
"sélectionner...": "Select...",
"toast.edit.error": "Failed to save changes",

View File

@ -102,6 +102,7 @@
"supprimer": "Supprimer",
"sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage",
"sélectionner": "Sélectionner",
"team": "Équipe",
"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",
@ -113,6 +114,9 @@
"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.team.update.error": "Erreur lors de la mise à jour de l'équipe",
"toast.team.update.pending": "Mise à jour de l'équipe...",
"toast.team.update.success": "Équipe mise à jour !",
"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 !",
@ -125,12 +129,8 @@
"toast.updateTrees.init.success": "Arbres créés !",
"toast.updateTrees.pending": "Mise à jour des arbres du tournoi...",
"toast.updateTrees.success": "Arbres mis à jour !",
"toast.team.update.error": "Erreur lors de la mise à jour de l'équipe",
"toast.team.update.pending": "Mise à jour de l'équipe...",
"toast.team.update.success": "Équipe mise à jour !",
"tournoi": "Tournoi",
"tournois": "Tournois",
"team": "Équipe",
"tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver",
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",

View File

@ -1,13 +1,15 @@
{
"(optionnelle)": "(optionnelle)",
"---SansClub---": "--- sans club ---",
"---ToutLesClubs---": "--- tout les clubs ---",
"---TousLesAges---": "--- tous les ages ---",
"---ToutLesClubs---": "--- tous 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",
"LesModificationsNontEnregistrer": "/!\\ Les modifications n'ont pas encore été enregistré, cliqué sur enregistrer /!\\",
"activer": "Activer",
"admin": "Administration",
"administrateur": "Administrateur",
@ -81,11 +83,14 @@
"ajouterUnClub": "Ajouter un club",
"ajouterUnMembre": "Ajouter un membre",
"all_season": "--- tout les saisons ---",
"arme": "Arme",
"au": "au",
"aucun": "Aucun",
"aucunMembreSélectionné": "Aucun membre sélectionné",
"aucuneCatégorieDisponible": "Aucune catégorie disponible pour le moment.",
"back": "« retour",
"blason": "Blason",
"bouclier": "Bouclier",
"bureau": "Bureau",
"button.accepter": "Accepter",
"button.ajouter": "Ajouter",
@ -93,7 +98,6 @@
"button.appliquer": "Appliquer",
"button.confirmer": "Confirmer",
"button.créer": "Créer",
"button.enregister": "Enregister",
"button.enregistrer": "Enregistrer",
"button.fermer": "Fermer",
"button.modifier": "Modifier",
@ -115,6 +119,7 @@
"cat.vétéran2": "Vétéran 2",
"categorie": "categorie",
"catégorie": "Catégorie",
"catégorieàAjouter": "Catégorie à ajouter",
"certificatMédical": "Certificat médical",
"chargement...": "Chargement...",
"chargerLexcel": "Charger l'Excel",
@ -249,6 +254,7 @@
"compte": "Compte",
"compétition": "Compétition",
"configuration": "Configuration",
"configurationDeLaCatégorie": "Configuration de la catégorie",
"conserverLancienEmail": "Conserver l'ancien email",
"contactAdministratif": "Contact administratif",
"contactInterne": "Contact interne",
@ -274,6 +280,8 @@
"donnéesAdministratives": "Données administratives",
"du": "Du",
"dun": "d'un",
"duréePause": "Durée pause",
"duréeRound": "Durée round",
"définirLidDuCompte": "Définir l'id du compte",
"editionDeL'affiliation": "Edition de l'affiliation",
"editionDeLaDemande": "Edition de la demande ",
@ -474,6 +482,7 @@
"permission": "Permission",
"photos": "Photos",
"prenom": "Prénom",
"protectionObligatoire": "Protection obligatoire",
"prénomEtNom": "Prénom et nom",
"rechercher": "Rechercher",
"rechercher...": "Rechercher...",
@ -494,8 +503,15 @@
"role.vise-secrétaire": "Vise-Secrétaire",
"role.vise-trésorier": "Vise-Trésorier",
"saison": "Saison",
"sans": "Sans",
"secrétariatsDeLice": "Secrétariats de lice",
"selectionner...": "Sélectionner...",
"shield.buckler": "Bocle",
"shield.none": "$t(sans) / $t(nonDéfinie)",
"shield.round": "Rond",
"shield.standard": "Standard",
"shield.teardrop": "Larme",
"siDisponiblePourLaCatégorieDages": "Si disponible pour la catégorie d'ages",
"siretOuRna": "SIRET ou RNA",
"stats": "Statistiques",
"statue": "Statue",
@ -505,6 +521,10 @@
"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 ?",
"sword.none": "$t(sans) / $t(nonDéfinie)",
"sword.oneHand": "Une main",
"sword.saber": "Sabre",
"sword.twoHand": "Deux mains",
"sélectionEnéquipeDeFrance": "Sélection en équipe de France",
"sélectionner...": "Sélectionner...",
"toast.edit.error": "Échec de l'enregistrement des modifications",

View File

@ -0,0 +1,99 @@
const ProtectionSelector = ({mandatoryProtection = 0, setMandatoryProtection = () => {} }) => {
const toggle = (bit) => {
setMandatoryProtection(v => (v & bit ? v & ~bit : v | bit));
};
const isOn = (bit) => (mandatoryProtection & bit) !== 0;
const color = (bit) => (isOn(bit) ? "#4ade80" : "#e5e7eb");
return (
<div>
<svg width="200" height="420" viewBox="0 0 160 320">
<rect
width="160" height="320"
fill="#f9fafb00"
stroke="#d1d5db"
strokeWidth="2"
rx="10"
/>
{/* Head */}
<ellipse
cx="80" cy="35" rx="20" ry="22"
fill={color(1)}
onClick={() => toggle(1)}
/>
{/* Throat / Neck */}
<ellipse
cx="80" cy="65" rx="12" ry="8"
fill={color(2)}
onClick={() => toggle(2)}
/>
{/* Torso */}
<ellipse
cx="80" cy="118" rx="30" ry="45"
fill={color(4)}
onClick={() => toggle(4)}
/>
{/* Arms */}
<ellipse
cx="38" cy="118" rx="12" ry="40"
fill={color(8)}
onClick={() => toggle(8)}
/>
<ellipse
cx="122" cy="118" rx="12" ry="40"
fill={color(8)}
onClick={() => toggle(8)}
/>
{/* Hands */}
<ellipse
cx="38" cy="170" rx="10" ry="12"
fill={color(16)}
onClick={() => toggle(16)}
/>
<ellipse
cx="122" cy="170" rx="10" ry="12"
fill={color(16)}
onClick={() => toggle(16)}
/>
{/* Legs */}
<ellipse
cx="65" cy="230" rx="14" ry="55"
fill={color(64)}
onClick={() => toggle(64)}
/>
<ellipse
cx="95" cy="230" rx="14" ry="55"
fill={color(64)}
onClick={() => toggle(64)}
/>
{/* Groin */}
<ellipse
cx="80" cy="170" rx="20" ry="10"
fill={color(32)}
onClick={() => toggle(32)}
/>
{/* Feet */}
<ellipse
cx="65" cy="295" rx="16" ry="8"
fill="#f0f0f0"
/>
<ellipse
cx="95" cy="295" rx="16" ry="8"
fill="#f0f0f0"
/>
</svg>
</div>
);
}
export default ProtectionSelector;

View File

@ -6,12 +6,23 @@ 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, getToastMessage} from "../../utils/Tools.js";
import {useEffect, useReducer, useState} from "react";
import {
apiAxios,
CatList,
getCatName, getShieldTypeName,
getSwordTypeName,
getToastMessage,
ShieldList,
sortCategories,
SwordList,
timePrint
} from "../../utils/Tools.js";
import React, {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";
import ProtectionSelector from "../../components/ProtectionSelector.jsx";
export function CompetitionEdit() {
const {id} = useParams()
@ -190,10 +201,10 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
}}><FontAwesomeIcon icon={faAdd}/></button>
</div>
</ul>
</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">{t('button.enregistrer')}</button>
<div className="row" style={{marginTop: "1em"}}>
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div>
</div>
</div>
</div>
@ -205,6 +216,9 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
function Content({data}) {
const navigate = useNavigate();
const [registerMode, setRegisterMode] = useState(data.registerMode || "FREE");
const [modaleState, setModaleState] = useState({})
const [presets, setPresets] = useState(data.presets || []);
const [presetChange, setPresetChange] = useState(false)
const {t} = useTranslation();
const handleSubmit = (event) => {
@ -227,6 +241,7 @@ function Content({data}) {
out['startRegister'] = event.target.startRegister?.value
out['endRegister'] = event.target.endRegister?.value
out['registerMode'] = registerMode
out['presets'] = presets
if (out['registerMode'] === "HELLOASSO") {
out['data3'] = event.target.data3?.value
@ -277,14 +292,17 @@ function Content({data}) {
toast.promise(
apiAxios.post(`/competition`, out), getToastMessage("comp.toast.save")
).then(data => {
setPresetChange(false)
console.log(data.data)
if (data.data.id !== undefined)
navigate("/competition/" + data.data.id)
if (data.data.presets !== undefined)
setPresets(data.data.presets)
})
}
return <form onSubmit={handleSubmit}>
<div className="card mb-4">
return <>
<form onSubmit={handleSubmit} className="card mb-4">
<input name="id" value={data.id || ""} readOnly hidden/>
<div className="card-header">{data.id ? t('comp.editionCompétition') : t('comp.créationCompétition')}</div>
<div className="card-body text-center">
@ -340,6 +358,40 @@ function Content({data}) {
</div>
</div>
<div className="accordion-item">
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour"
aria-expanded="false" aria-controls="collapseFour">
Catégories proposées
</button>
</h2>
<div id="collapseFour" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div className="accordion-body" style={{textAlign: "left"}}>
<div className="list-group">
{presets.sort((a, b) => a.name.localeCompare(b.name)).map((preset) =>
<a key={preset.id} className="list-group-item list-group-item-action" data-bs-toggle="modal"
data-bs-target="#catModal" onClick={() => setModaleState(preset)}>
<span style={{margin: "0 0.25em 0 0"}}>{preset.name}</span>
{preset.categories.sort(sortCategories).map((cat, index) =>
<span key={index} className="badge text-bg-secondary"
style={{margin: "0 0.25em"}}>{getCatName(cat)}</span>)}
</a>)}
</div>
<div className="row" style={{marginTop: "1em"}}>
<div className="col-auto"
style={{color: "red"}}>{presetChange && t('LesModificationsNontEnregistrer')}</div>
<div className="col" style={{textAlign: "right"}}>
<button type="button" className="btn btn-success" data-bs-toggle="modal"
data-bs-target="#catModal"
onClick={() => setModaleState({id: Math.min(...presets.map(p => p.id), 0) - 1})}>
<FontAwesomeIcon icon={faAdd}/>
</button>
</div>
</div>
</div>
</div>
</div>
<div className="accordion-item">
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree"
@ -404,14 +456,162 @@ function Content({data}) {
</div>
</div>
<div className="row" style={{marginTop: "1em"}}>
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div>
</div>
</div>
</div>
</form>
<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">{t('button.enregistrer')}</button>
<div className="modal fade" id="catModal" tabIndex="-1" aria-labelledby="catModalLabel" aria-hidden="true">
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
<div className="modal-content">
<CatModalContent setPresets={setPresets} setPresetChange={setPresetChange} state={modaleState}/>
</div>
</div>
</div>
</form>
</>
}
function CatModalContent({setPresets, setPresetChange, state}) {
const [name, setName] = useState(state.name || "")
const [sword, setSword] = useState(state.sword || "NONE")
const [shield, setShield] = useState(state.shield || "NONE")
const [time, setTime] = useState(timePrint(state.roundDuration || 90000))
const [pause, setPause] = useState(timePrint(state.pauseDuration || 60000))
const [cats, setCats] = useState(state.categories || [])
const [mandatoryProtection, setMandatoryProtection] = useState(state.mandatoryProtection || 33)
const {t} = useTranslation();
useEffect(() => {
setName(state.name || "")
setSword(state.sword || "NONE")
setShield(state.shield || "NONE")
setTime(timePrint(state.roundDuration || 90000))
setPause(timePrint(state.pauseDuration || 60000))
setCats(state.categories || [])
setMandatoryProtection(state.mandatoryProtection || 33)
}, [state]);
const setCat = (e, cat) => {
if (e.target.checked) {
if (!cats.includes(cat)) {
setCats([...cats, cat])
}
} else {
setCats(cats.filter(c => c !== cat))
}
}
const isCatSelected = (cat) => cats.includes(cat)
const parseTime = (str) => {
const parts = str.split(":").map(part => parseInt(part, 10));
if (parts.length === 1) {
return parts[0] * 1000;
} else if (parts.length === 2) {
return (parts[0] * 60 + parts[1]) * 1000;
} else {
return 0;
}
}
const handleSave = () => {
const out = {
id: state.id,
name: name,
sword: sword,
shield: shield,
roundDuration: parseTime(time),
pauseDuration: parseTime(pause),
categories: cats,
mandatoryProtection: mandatoryProtection
}
setPresets(presets => [...presets.filter(p => p.id !== out.id), out])
setPresetChange(true)
}
const handleRm = () => {
setPresets(presets => presets.filter(p => p.id !== state.id))
setPresetChange(true)
}
return <>
<div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">{t('configurationDeLaCatégorie')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="row">
<div className="col-12 col-md-7 mb-3">
<div className="input-group mb-3">
<span className="input-group-text" id="categorie">{t("nom")}</span>
<input type="text" className="form-control" placeholder={t("nom")} name="name"
value={name} onChange={e => setName(e.target.value)}/>
</div>
<div className="input-group mb-3">
<span className="input-group-text">{t('duréeRound')}</span>
<input type="text" className="form-control" placeholder="0" aria-label="Min" value={time}
onChange={e => setTime(e.target.value)}/>
<span className="input-group-text">(mm:ss)</span>
</div>
<div className="input-group mb-3">
<span className="input-group-text">{t('duréePause')}</span>
<input type="text" className="form-control" placeholder="0" aria-label="Min" value={pause}
onChange={e => setPause(e.target.value)}/>
<span className="input-group-text">(mm:ss)</span>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="sword">{t('arme')}</span>
<select className="form-select" aria-label={t('arme')} name="sword" value={sword}
onChange={e => setSword(e.target.value)}>
{SwordList.map(sword =>
<option key={sword} value={sword}>{getSwordTypeName(sword)}</option>
)}
</select>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="shield">{t('bouclier')}</span>
<select className="form-select" aria-label={t('bouclier')} name="shield" value={shield}
onChange={e => setShield(e.target.value)}>
{ShieldList.map(shield =>
<option key={shield} value={shield}>{getShieldTypeName(shield)}</option>
)}
</select>
</div>
<label htmlFor="inputState2" className="form-label">
{t('catégorie')} :
</label>
<div className="d-flex flex-wrap">
{CatList.map((cat, index) => <div key={index} className="input-group" style={{display: "contents"}}>
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox" id={"catInput" + index} checked={isCatSelected(cat)}
aria-label={getCatName(cat)} onChange={e => setCat(e, cat)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={"catInput" + index}>{getCatName(cat)}</label>
</div>
</div>)}
</div>
</div>
<div className="col-12 col-md-5">
<div style={{textAlign: "center"}}>
<h6>{t('protectionObligatoire')} :</h6>
<ProtectionSelector mandatoryProtection={mandatoryProtection} setMandatoryProtection={setMandatoryProtection}/>
</div>
</div>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={handleRm}>{t('button.supprimer')}</button>
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
<button type="button" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSave}>{t('button.appliquer')}</button>
</div>
</>
}

View File

@ -3,8 +3,8 @@ import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useEffect, useReducer, useRef, useState} from "react";
import {apiAxios, CatList, getCatName, getToastMessage} from "../../utils/Tools.js";
import React, {useEffect, useReducer, useRef, useState} from "react";
import {apiAxios, applyOverCategory, CatList, getCatName, getToastMessage} from "../../utils/Tools.js";
import {toast} from "react-toastify";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@ -19,12 +19,14 @@ export function CompetitionRegisterAdmin({source}) {
const navigate = useNavigate()
const [state, dispatch] = useReducer(SimpleReducer, [])
const [clubFilter, setClubFilter] = useState("")
const [catFilter, setCatFilter] = useState("")
const [catAgeFilter, setCatAgeFilter] = useState("")
const [catFilter, setCatFilter] = useState(-1)
const [modalState, setModalState] = useState({})
const {t} = useTranslation();
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/competition/${id}/register/${source}`, setLoading, 1)
const {data: data2, error: error2} = useFetch(`/competition/${id}/categories`, setLoading, 1)
const sortName = (a, b) => {
if (a.data.fname === b.data.fname) return a.data.lname.localeCompare(b.data.lname);
@ -43,11 +45,11 @@ export function CompetitionRegisterAdmin({source}) {
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), getToastMessage("comp.toast.register.add")
).then((response) => {
if (response.data.error) {
return
return null;
}
dispatch({type: 'UPDATE_OR_ADD', payload: {id: response.data.id, data: response.data}})
dispatch({type: 'SORT', payload: sortName})
document.getElementById("closeModal").click();
return response.data
})
}
@ -62,7 +64,9 @@ export function CompetitionRegisterAdmin({source}) {
<div className="col-lg-9">
{data ? <div className="">
<MakeCentralPanel
data={state.filter(s => (clubFilter.length === 0 || s.data.club.name === clubFilter) && (catFilter.length === 0 || s.data.categorie === catFilter))}
data={state.filter(s => (clubFilter.length === 0 || s.data.club.name === clubFilter)
&& (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter)
&& (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter)))}
dispatch={dispatch} id={id} setModalState={setModalState} source={source}/>
</div> : error ? <AxiosError error={error}/> : <Def/>}
</div>
@ -77,36 +81,60 @@ export function CompetitionRegisterAdmin({source}) {
onClick={() => setModalState({id: -793548328091516928})}>{t('comp.ajouterUnInvité')}
</button>
</div>}
<QuickAdd sendRegister={sendRegister} source={source}/>
<QuickAdd sendRegister={sendRegister} source={source} data2={data2} error2={error2}/>
<div className="card mb-4">
<div className="card-header">{t('filtre')}</div>
<div className="card-body">
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} catFilter={catFilter}
setCatFilter={setCatFilter} source={source}/>
<FiltreBar data={data} data2={data2} clubFilter={clubFilter} setClubFilter={setClubFilter} catFilter={catFilter}
setCatFilter={setCatFilter} catAgeFilter={catAgeFilter} setCatAgeFilter={setCatAgeFilter} source={source}/>
</div>
</div>
{source === "admin" && <FileOutput data={data}/>}
</div>
</div>
<Modal sendRegister={sendRegister} modalState={modalState} setModalState={setModalState} source={source}/>
<Modal data2={data2} error2={error2} sendRegister={sendRegister} modalState={modalState} setModalState={setModalState} source={source}/>
</div>
}
function QuickAdd({sendRegister, source}) {
function QuickAdd({sendRegister, source, data2, error2}) {
const {t} = useTranslation();
const [categories, setCategories] = useState([])
const handleAdd = (licence) => {
console.log("Quick add licence: " + licence)
sendRegister({
licence: licence, fname: "", lname: "", weight: "", overCategory: 0, lockEdit: false, id: null
licence: licence,
fname: "",
lname: "",
weight: "",
overCategory: 0,
lockEdit: false,
id: null,
quick: true,
categoriesInscrites: categories
})
}
const setCategories_ = (e, catId) => {
if (e.target.checked) {
if (!categories.includes(catId)) {
setCategories([...categories, catId])
}
} else {
setCategories(categories.filter(c => c !== catId))
}
}
return <div className="card mb-4">
<div className="card-header">{t('comp.ajoutRapide')}</div>
<div className="card-body">
<div className="d-flex flex-wrap">
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
{t('catégorieàAjouter')}<br/> <small>({t('siDisponiblePourLaCatégorieDages')})</small>
</label>
<CategoriesList error2={error2} availableCats={data2} categories={categories} setCategories={setCategories_}/>
</div>
<div className="row">
<span>{t('comp.noDeLicence')}</span>
</div>
@ -131,14 +159,14 @@ function QuickAdd({sendRegister, source}) {
</button>
{source === "club" && <LoadingProvider>
<SearchMember sendRegister={sendRegister}/>
<SearchMember sendRegister={sendRegister} categories={categories}/>
</LoadingProvider>}
</div>
</div>
</div>
}
function SearchMember({sendRegister}) {
function SearchMember({sendRegister, categories}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/members`, setLoading, 1)
const [suggestions, setSuggestions] = useState([])
@ -158,7 +186,9 @@ function SearchMember({sendRegister}) {
weight: "",
overCategory: 0,
lockEdit: false,
id: null
id: null,
quick: true,
categoriesInscrites: categories
})
}
@ -284,9 +314,33 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
</div>);
};
function Modal({sendRegister, modalState, setModalState, source}) {
function CategoriesList({error2, availableCats, fistCatInput, categories, setCategories}) {
const {t} = useTranslation();
return <>
{error2 ? <AxiosError error={error2}/> : <>
{availableCats && availableCats.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
{availableCats && availableCats.map((cat, index) =>
<div key={cat.id} className="input-group"
style={{display: "contents"}}>
<div className="input-group-text">
<input ref={index === 0 ? fistCatInput : undefined} className="form-check-input mt-0" type="checkbox"
id={"categoriesInput" + index} checked={categories.includes(cat.id)} aria-label={cat.name}
onChange={e => setCategories(e, cat.id)}/>
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{cat.name}</label>
</div>
</div>)}
</>}
</>
}
function Modal({data2, error2, sendRegister, modalState, setModalState, source}) {
const country = useCountries('fr')
const {t} = useTranslation();
const closeBtn = useRef(null);
const licenceInput = useRef(null);
const nameInput = useRef(null);
const fistCatInput = useRef(null);
const submitBtn = useRef(null);
const [licence, setLicence] = useState("")
const [fname, setFname] = useState("")
@ -299,97 +353,125 @@ function Modal({sendRegister, modalState, setModalState, source}) {
const [genre, setGenre] = useState("NA")
const [editMode, setEditMode] = useState(false)
const [lockEdit, setLockEdit] = useState(false)
const [categories, setCategories] = useState([])
useEffect(() => {
console.log(modalState)
if (!modalState) {
setLicence("")
setFname("")
setLname("")
setWeight("")
setCat(0)
setEditMode(false)
setLockEdit(false)
setClub("")
setGCat("")
setCountry_("FR")
setGenre("NA")
} else {
setLicence(modalState.licence ? modalState.licence : "")
setFname(modalState.fname ? modalState.fname : "")
setLname(modalState.lname ? modalState.lname : "")
setWeight(modalState.weight ? modalState.weight : "")
setCat(modalState.overCategory ? modalState.overCategory : 0)
setEditMode(modalState.licence || (modalState.fname && modalState.lname))
setLockEdit(modalState.lockEdit)
setClub(modalState.club ? modalState.club.name : "")
setGCat(modalState.categorie ? modalState.categorie : "")
setCountry_(modalState.country ? modalState.country : "FR")
setGenre(modalState.genre ? modalState.genre : "NA")
}
setLicence(modalState?.licence ? modalState.licence : "")
setFname(modalState?.fname ? modalState.fname : "")
setLname(modalState?.lname ? modalState.lname : "")
setWeight(modalState?.weight ? modalState.weight : "")
setCat(modalState?.overCategory ? modalState.overCategory : 0)
setEditMode(modalState?.licence || (modalState.fname && modalState.lname))
setLockEdit(modalState?.lockEdit === undefined ? false : modalState.lockEdit)
setClub(modalState?.club ? modalState.club.name : "")
setGCat(modalState?.categorie ? modalState.categorie : "")
setCountry_(modalState?.country ? modalState.country : "FR")
setGenre(modalState?.genre ? modalState.genre : "NA")
setCategories(modalState?.categoriesInscrites ? modalState.categoriesInscrites : [])
setTimeout(() => {
if (modalState?.id === 0) {
licenceInput.current?.focus()
} else if (modalState?.id < 0) {
nameInput.current?.focus()
}
}, 450)
}, [modalState]);
return <div className="modal fade" id="registerModal" tabIndex="-1" aria-labelledby="registerLabel"
aria-hidden="true">
const setCategories_ = (e, catId) => {
if (e.target.checked) {
if (!categories.includes(catId)) {
setCategories([...categories, catId])
}
} else {
setCategories(categories.filter(c => c !== catId))
}
}
const handleSubmit = (e) => {
e.preventDefault()
const new_state = {
licence: Number.isInteger(licence) ? licence : licence.trim(),
fname: fname.trim(),
lname: lname.trim(),
weight: weight,
overCategory: cat,
lockEdit: lockEdit,
categoriesInscrites: categories,
id: modalState.id !== 0 ? modalState.id : null
}
if (modalState.id < 0) {
new_state.licence = -1
new_state.categorie = gcat
new_state.club = club
new_state.country = country_
new_state.genre = genre
}
sendRegister(new_state)
.then(data => {
if (!data) return;
setModalState(data)
if (editMode || data.id < 0) {
closeBtn.current.click()
} else {
setTimeout(() => {
if (fistCatInput.current) {
fistCatInput.current.focus()
} else {
submitBtn.current.focus()
}
}, 100)
}
})
}
const currenCat = gcat !== "" ? applyOverCategory(gcat, cat) : "";
const availableCats = data2 ? (currenCat !== "" ? data2.filter(c => c.categories.includes(currenCat)) : data2).sort((a, b) => a.name.localeCompare(b.name)) : []
if (availableCats.length === 0) {
if (fistCatInput.current) {
fistCatInput.current = null
}
}
return <div className="modal fade" id="registerModal" tabIndex="-1" aria-labelledby="registerLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<form onSubmit={e => {
e.preventDefault()
const new_state = {
licence: Number.isInteger(licence) ? licence : licence.trim(),
fname: fname.trim(),
lname: lname.trim(),
weight: weight,
overCategory: cat,
lockEdit: lockEdit,
id: modalState.id !== 0 ? modalState.id : null
}
if (modalState.id < 0) {
new_state.licence = -1
new_state.categorie = gcat
new_state.club = club
new_state.country = country_
new_state.genre = genre
}
sendRegister(new_state)
.then(() => {
setModalState(new_state)
})
}}>
<div className="modal-header">
<h1 className="modal-title fs-5"
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">{t('comp.modal.text1')}</div>}
<div className="card" style={{marginBottom: "1em"}}>
<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={t("comp.noDeLicence")}
name="licence"
value={licence} onChange={e => setLicence(e.target.value)} disabled={editMode}/>
</div>
<div className="modal-header">
<h1 className="modal-title fs-5"
id="registerLabel">{editMode ? t('modification') : t('ajout')} {t('dun')} {modalState.id >= 0 ? t('combattant') : t('invité')}</h1>
<button ref={closeBtn} 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">{t('comp.modal.text1')}</div>}
<div className="card" style={{marginBottom: "1em"}}>
<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 ref={licenceInput} 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}
onKeyUp={e => e.key === "Enter" ? handleSubmit(e) : undefined}/>
</div>
<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={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={t('nom')} name="lname"
disabled={editMode && modalState.id >= 0}
value={lname} onChange={e => setLname(e.target.value)}/>
</div>
</div>
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>{t('ou')}</h5>
<div className="row">
<div className="col">
<input ref={nameInput} 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={t('nom')} name="lname"
disabled={editMode && modalState.id >= 0} value={lname} onChange={e => setLname(e.target.value)}
onKeyUp={e => e.key === "Enter" && modalState.id >= 0 ? handleSubmit(e) : undefined}/>
</div>
</div>
</div>
</div>
{(editMode || modalState.id < 0) && <>
<div className="input-group mb-3" hidden={modalState.id >= 0}>
<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"
@ -451,28 +533,34 @@ function Modal({sendRegister, modalState, setModalState, source}) {
onChange={e => setLockEdit(e.target.checked)}/>
<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 ? 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 className="d-flex flex-wrap">
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
{t('catégorie')} :
</label>
<CategoriesList error2={error2} availableCats={availableCats} fistCatInput={fistCatInput} categories={categories}
setCategories={setCategories_}/>
</div>
</>}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">{t('button.annuler')}</button>
<button ref={submitBtn} type="button" className="btn btn-primary"
onClick={handleSubmit}>{editMode ? t('button.modifier') : t('button.ajouter')}</button>
</div>
</div>
</div>
</div>
}
let allClub = []
let allCat = []
function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, source}) {
function FiltreBar({data, data2, clubFilter, setClubFilter, catFilter, setCatFilter, catAgeFilter, setCatAgeFilter, source}) {
const {t} = useTranslation();
useEffect(() => {
if (!data) return;
allClub.push(...data.map((e) => e.club?.name))
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
allCat.push(...data.map((e) => e.categorie))
allCat = allCat.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
}, [data]);
return <div>
@ -485,13 +573,19 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
</select>
</div>}
<div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">{t('---TouteLesCatégories---')}</option>
{allCat && allCat.map((value, index) => {
return <option key={index} value={value}>{value}</option>
<select className="form-select" value={catAgeFilter} onChange={event => setCatAgeFilter(event.target.value)}>
<option value="">{t('---TousLesAges---')}</option>
{CatList && CatList.map((value, index) => {
return <option key={index} value={value}>{getCatName(value)}</option>
})}
</select>
</div>
<select className="form-select" value={catFilter} onChange={event => setCatFilter(Number(event.target.value))}>
<option value={-1}>{t('---TouteLesCatégories---')}</option>
{data2 && data2.map((cat) => {
return <option key={cat.id} value={cat.id}>{cat.name}</option>
})}
</select>
</div>
}

View File

@ -160,7 +160,7 @@ function SelfRegister({data2}) {
onClick={handleUnregister}>{t('button.seDésinscrire')}
</button>
<button type="button" className="btn btn-primary" disabled={disabled}
onClick={handleSubmit}>{t('button.enregister')}
onClick={handleSubmit}>{t('button.enregistrer')}
</button>
</div>
</div>

View File

@ -42,6 +42,32 @@ export const CatList = [
"VETERAN2"
];
export function sortCategories(catA, catB) {
const indexA = CatList.indexOf(catA);
const indexB = CatList.indexOf(catB);
if (indexA === -1 && indexB === -1) {
return 0; // Both categories are unknown, maintain their order
} else if (indexA === -1) {
return 1; // catA is unknown, place it after catB
} else if (indexB === -1) {
return -1; // catB is unknown, place it after catA
} else {
return indexA - indexB; // Both categories are known, sort by their indices
}
}
export function applyOverCategory(cat, overCat) {
const catIndex = CatList.indexOf(cat) + overCat;
if (catIndex < 0) {
return CatList[0];
} else if (catIndex >= CatList.length) {
return CatList[CatList.length - 1];
} else {
return CatList[catIndex];
}
}
export function getCategoryFormBirthDate(birth_date, currentDate = new Date()) {
const currentSaison = getSaison(currentDate)
const birthYear = birth_date.getFullYear()
@ -111,6 +137,53 @@ export function getCatName(cat) {
}
}
export const SwordList = [
"NONE",
"ONE_HAND",
"TWO_HAND",
"SABER"
]
export function getSwordTypeName(type) {
switch (type) {
case "NONE":
return i18n.t('sword.none');
case "ONE_HAND":
return i18n.t('sword.oneHand');
case "TWO_HAND":
return i18n.t('sword.twoHand');
case "SABER":
return i18n.t('sword.saber');
default:
return type;
}
}
export const ShieldList = [
"NONE",
"STANDARD",
"ROUND",
"TEARDROP",
"BUCKLER"
]
export function getShieldTypeName(type) {
switch (type) {
case "NONE":
return i18n.t('shield.none');
case "STANDARD":
return i18n.t('shield.standard');
case "ROUND":
return i18n.t('shield.round');
case "TEARDROP":
return i18n.t('shield.teardrop');
case "BUCKLER":
return i18n.t('shield.buckler');
default:
return type;
}
}
export function getToastMessage(msgKey, ns = 'common') {
return {
pending: i18n.t(msgKey + '.pending', {ns}),