From d1c7f37a945966cb87bfe92f2afbf654605ff286 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 3 Sep 2025 18:56:58 +0200 Subject: [PATCH 01/59] feat: competition result --- .../ffsaf/data/model/MatchModel.java | 27 ++ .../ffsaf/data/model/RegisterModel.java | 10 + .../ffsaf/domain/service/ResultService.java | 400 ++++++++++++++++++ .../ffsaf/rest/ResultEndpoints.java | 48 +++ .../ffsaf/rest/data/ResultCategoryData.java | 59 +++ .../fr/titionfire/ffsaf/utils/TreeNode.java | 35 ++ src/main/resources/lang/String.properties | 16 + src/main/webapp/public/img/171891.png | Bin 0 -> 12575 bytes src/main/webapp/src/App.jsx | 6 + src/main/webapp/src/components/Nav.jsx | 20 +- .../webapp/src/pages/result/DrawGraph.jsx | 252 +++++++++++ .../webapp/src/pages/result/ResultList.jsx | 49 +++ .../webapp/src/pages/result/ResultRoot.jsx | 26 ++ .../webapp/src/pages/result/ResultView.jsx | 347 +++++++++++++++ 14 files changed, 1294 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java create mode 100644 src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java create mode 100644 src/main/resources/lang/String.properties create mode 100644 src/main/webapp/public/img/171891.png create mode 100644 src/main/webapp/src/pages/result/DrawGraph.jsx create mode 100644 src/main/webapp/src/pages/result/ResultList.jsx create mode 100644 src/main/webapp/src/pages/result/ResultRoot.jsx create mode 100644 src/main/webapp/src/pages/result/ResultView.jsx diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java index a20694e..22765ea 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -53,4 +53,31 @@ public class MatchModel { List scores = new ArrayList<>(); char poule = 'A'; + + + public String getC1Name() { + if (c1_id == null) + return c1_str; + return c1_id.fname + " " + c1_id.lname; + } + + public String getC2Name() { + if (c2_id == null) + return c2_str; + return c2_id.fname + " " + c2_id.lname; + } + + public int win() { + int sum = 0; + for (ScoreEmbeddable score : this.getScores()) { + if (score.getS1() == -1000 || score.getS2() == -1000) + continue; + + if (score.getS1() > score.getS2()) + sum++; + else if (score.getS1() < score.getS2()) + sum--; + } + return sum; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 19f84ab..953a637 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -53,4 +53,14 @@ public class RegisterModel { this.categorie = categorie; this.club = club; } + + public String getName() { + return membre.fname + " " + membre.lname; + } + + public ClubModel getClub2() { + if (club == null) + return membre.club; + return club; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java new file mode 100644 index 0000000..f8cb994 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -0,0 +1,400 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.model.*; +import fr.titionfire.ffsaf.data.repository.CategoryRepository; +import fr.titionfire.ffsaf.data.repository.CompetitionRepository; +import fr.titionfire.ffsaf.data.repository.MatchRepository; +import fr.titionfire.ffsaf.data.repository.RegisterRepository; +import fr.titionfire.ffsaf.rest.data.ResultCategoryData; +import fr.titionfire.ffsaf.rest.exception.DForbiddenException; +import fr.titionfire.ffsaf.utils.*; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.Builder; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +@WithSession +@ApplicationScoped +public class ResultService { + + @Inject + CompetitionRepository compRepository; + + @Inject + RegisterRepository registerRepository; + + @Inject + MembreService membreService; + + @Inject + CategoryRepository categoryRepository; + + @Inject + MatchRepository matchRepository; + + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String"); + + public Uni> getList(SecurityCtx securityCtx) { + return membreService.getByAccountId(securityCtx.getSubject()) + .chain(m -> registerRepository.list("membre = ?1", m)) + .onItem().transformToMulti(Multi.createFrom()::iterable) + .onItem().call(r -> Mutiny.fetch(r.getCompetition())) + .onItem().transform(r -> new Object[]{r.getCompetition().getUuid(), r.getCompetition().getName(), + r.getCompetition().getDate()}) + .collect().asList(); + } + + public Uni> getCategory(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx) + .chain(m -> categoryRepository.list("compet.uuid = ?1", uuid) + .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1) OR category IN ?2", //TODO AND + m.getMembre(), cats))) + .map(matchModels -> { + HashMap> map = new HashMap<>(); + for (MatchModel matchModel : matchModels) { + if (!map.containsKey(matchModel.getCategory().getId())) + map.put(matchModel.getCategory().getId(), new ArrayList<>()); + map.get(matchModel.getCategory().getId()).add(matchModel); + } + + return map.values(); + }) + .onItem() + .transformToMulti(Multi.createFrom()::iterable) + .onItem().call(list -> Mutiny.fetch(list.get(0).getCategory().getTree())) + .onItem().transform(this::getData) + .collect().asList(); + + } + + private ResultCategoryData getData(List matchModels) { + ResultCategoryData out = new ResultCategoryData(); + + CategoryModel categoryModel = matchModels.get(0).getCategory(); + out.setName(categoryModel.getName()); + out.setType(categoryModel.getType()); + + getArray2(matchModels, out); + getTree(categoryModel.getTree(), out); + return out; + } + + private void getArray2(List matchModels_, ResultCategoryData out) { + List matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList(); + + HashMap> matchMap = new HashMap<>(); + for (MatchModel model : matchModels) { + char g = model.getPoule(); + if (!matchMap.containsKey(g)) + matchMap.put(g, new ArrayList<>()); + matchMap.get(g).add(model); + } + + matchMap.forEach((c, matchEntities) -> { + List matchs = matchEntities.stream() + .sorted(Comparator.comparing(MatchModel::getCategory_ord)) + .map(ResultCategoryData.PouleArrayData::fromModel) + .toList(); + + List rankArray = matchEntities.stream() + .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) + .distinct() + .map(combName -> { + AtomicInteger w = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(0); + AtomicInteger pointTake = new AtomicInteger(0); + + matchEntities.stream() + .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) || m.getC2Name() + .equals(combName))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((matchModel.getC1Name() + .equals(combName) && win > 0) || matchModel.getC2Name() + .equals(combName) && win < 0) + w.getAndIncrement(); + + for (ScoreEmbeddable score : matchModel.getScores()) { + if (score.getS1() <= -900 || score.getS2() <= -900) + continue; + if (matchModel.getC1Name().equals(combName)) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + } + }); + float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get(); + + return new ResultCategoryData.RankArray(0, combName, w.get(), + pointMake.get(), pointTake.get(), pointRate); + }) + .sorted(Comparator + .comparing(ResultCategoryData.RankArray::getWin) + .thenComparing(ResultCategoryData.RankArray::getPointRate).reversed()) + .toList(); + out.getMatchs().put(c, matchs); + + int lastWin = -1; + float pointRate = 0; + int rank = 0; + for (ResultCategoryData.RankArray rankArray1 : rankArray) { + if (rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) { + lastWin = rankArray1.getWin(); + pointRate = rankArray1.getPointRate(); + rank++; + } + rankArray1.setRank(rank); + } + out.getRankArray().put(c, rankArray); + }); + } + + private static void convertTree(TreeModel src, TreeNode dst) { + dst.setData(ResultCategoryData.TreeData.from(src.getMatch())); + if (src.getLeft() != null) { + dst.setLeft(new TreeNode<>()); + convertTree(src.getLeft(), dst.getLeft()); + } + if (src.getRight() != null) { + dst.setRight(new TreeNode<>()); + convertTree(src.getRight(), dst.getRight()); + } + } + + private void getTree(List treeModels, ResultCategoryData out) { + ArrayList> trees = new ArrayList<>(); + treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> { + TreeNode root = new TreeNode<>(); + convertTree(treeModel, root); + trees.add(root); + }); + out.setTrees(trees); + } + + public Uni getAllCombArray(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx) + .chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); + + CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); + + List combs = matchModels.stream() + .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) + .filter(Objects::nonNull) + .distinct() + .map(combName -> { + var builder2 = CombsArrayData.CombsData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + matchModels.stream() + .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) + || m.getC2Name().equals(combName))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((combName.equals(matchModel.getC1Name()) && win > 0) || + combName.equals(matchModel.getC2Name()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } + + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (combName.equals(matchModel.getC1Name())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); + + Categorie categorie = null; + ClubModel club = null; + + Optional register = registers.stream() + .filter(r -> r.getName().equals(combName)).findFirst(); + if (register.isPresent()) { + categorie = register.get().getCategorie(); + club = register.get().getClub2(); + } + + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(combName); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + + return builder2.build(); + }) + .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) + .toList(); + + builder.nb_insc(combs.size()); + builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); + builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); + builder.combs(combs); + + return builder.build(); + }) + ); + } + + + @Builder + @RegisterForReflection + public static record CombsArrayData(int nb_insc, int tt_match, long point, List combs) { + @Builder + @RegisterForReflection + public static record CombsData(String cat, String club, String name, int w, int l, float ratioVictoire, + float ratioPoint, int pointMake, int pointTake) { + } + } + + public Uni getClubArray(String uuid, SecurityCtx securityCtx) { + ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder(); + + return hasAccess(uuid, securityCtx) + .invoke(cm_register -> builder.name(cm_register.getClub2().getName())) + .chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, + cm_register.getClub2()) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); + + builder.nb_insc(registers.size()); + + AtomicInteger tt_win = new AtomicInteger(0); + AtomicInteger tt_match = new AtomicInteger(0); + + List combData = registers.stream() + .map(register -> { + + var builder2 = ClubArrayData.CombData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + matchModels.stream() + .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) + || register.getMembre().equals(m.getC2_id()))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((register.getMembre() + .equals(matchModel.getC1_id()) && win > 0) || + register.getMembre() + .equals(matchModel.getC2_id()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } + + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (register.getMembre() + .equals(matchModel.getC1_id())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); + + Categorie categorie = register.getCategorie(); + if (categorie == null) + categorie = register.getMembre().getCategorie(); + + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(register.getName()); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + + tt_win.addAndGet(w.get()); + tt_match.addAndGet(w.get() + l.get()); + + return builder2.build(); + }) + .sorted(Comparator.comparing(ClubArrayData.CombData::name)) + .toList(); + + builder.nb_match(tt_match.get()); + builder.match_w(tt_win.get()); + builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); + builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); + builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); + builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); + builder.combs(combData); + + return builder.build(); + }) + ); + } + + @Builder + @RegisterForReflection + public static record ClubArrayData(String name, int nb_insc, int nb_match, int match_w, float ratioVictoire, + float ratioPoint, int pointMake, int pointTake, List combs) { + @Builder + @RegisterForReflection + public static record CombData(String cat, String name, int w, int l, float ratioVictoire, + float ratioPoint, int pointMake, int pointTake) { + } + } + + private Uni hasAccess(String uuid, SecurityCtx securityCtx) { + return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid) + .firstResult() + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DForbiddenException("Access denied"); + })); + } + + private Uni hasAccess(Long compId, SecurityCtx securityCtx) { + return registerRepository.find("membre.userId = ?1 AND competition.id = ?2", securityCtx.getSubject(), compId) + .firstResult() + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DForbiddenException("Access denied"); + })); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java new file mode 100644 index 0000000..4016887 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/ResultEndpoints.java @@ -0,0 +1,48 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.ResultService; +import fr.titionfire.ffsaf.rest.data.ResultCategoryData; +import fr.titionfire.ffsaf.utils.SecurityCtx; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import java.util.List; + +@Authenticated +@Path("api/result") +public class ResultEndpoints { + + @Inject + ResultService resultService; + + @Inject + SecurityCtx securityCtx; + + @GET + @Path("list") + public Uni> getList() { + return resultService.getList(securityCtx); + } + + @GET + @Path("{uuid}") + public Uni> getCategory(@PathParam("uuid") String uuid) { + return resultService.getCategory(uuid, securityCtx); + } + + @GET + @Path("{uuid}/club") + public Uni getClub(@PathParam("uuid") String uuid) { + return resultService.getClubArray(uuid, securityCtx); + } + + @GET + @Path("{uuid}/comb") + public Uni getComb(@PathParam("uuid") String uuid) { + return resultService.getAllCombArray(uuid, securityCtx); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java new file mode 100644 index 0000000..2054e52 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -0,0 +1,59 @@ +package fr.titionfire.ffsaf.rest.data; + +import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.utils.ScoreEmbeddable; +import fr.titionfire.ffsaf.utils.TreeNode; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Data +@NoArgsConstructor +@RegisterForReflection +public class ResultCategoryData { + int type; + String name; + HashMap> matchs = new HashMap<>(); + HashMap> rankArray = new HashMap<>(); + ArrayList> trees; + + @Data + @AllArgsConstructor + @RegisterForReflection + public static class RankArray { + int rank; + String name; + int win; + int pointMake; + int pointTake; + float pointRate; + } + + @RegisterForReflection + public record PouleArrayData(String red, boolean red_w, List score, boolean blue_w, String blue, boolean end) { + public static PouleArrayData fromModel(MatchModel matchModel) { + return new PouleArrayData( + matchModel.getC1Name(), + matchModel.isEnd() && matchModel.win() > 0, + matchModel.isEnd() ? + matchModel.getScores().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList() + : new ArrayList<>(), + matchModel.isEnd() && matchModel.win() < 0, + matchModel.getC2Name(), + matchModel.isEnd()); + } + } + + @RegisterForReflection + public static record TreeData(long id, String c1FullName, String c2FullName, List scores, + boolean end) { + public static TreeData from(MatchModel match) { + return new TreeData(match.getId(), match.getC1Name(), match.getC2Name(), match.getScores(), match.isEnd()); + } + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java b/src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java new file mode 100644 index 0000000..c175692 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/utils/TreeNode.java @@ -0,0 +1,35 @@ +package fr.titionfire.ffsaf.utils; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection +public class TreeNode { + private T data; + private TreeNode left; + private TreeNode right; + + public TreeNode(T data) { + this(data, null, null); + } + + public int death() { + int dg = 0; + int dd = 0; + + if (this.right != null) + dg = this.right.death(); + + if (this.left != null) + dg = this.left.death(); + + return 1 + Math.max(dg, dd); + } +} diff --git a/src/main/resources/lang/String.properties b/src/main/resources/lang/String.properties new file mode 100644 index 0000000..7e87f97 --- /dev/null +++ b/src/main/resources/lang/String.properties @@ -0,0 +1,16 @@ +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 \ No newline at end of file diff --git a/src/main/webapp/public/img/171891.png b/src/main/webapp/public/img/171891.png new file mode 100644 index 0000000000000000000000000000000000000000..4ffcd10772fe3b60c4a5769a155a805cce6ec5ca GIT binary patch literal 12575 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 Tz8i}R;Ag|yEo%x^`Jem`la(1) literal 0 HcmV?d00001 diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index bc53786..1cee2e9 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -14,6 +14,7 @@ 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 {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx"; const router = createBrowserRouter([ { @@ -53,6 +54,11 @@ const router = createBrowserRouter([ element: , children: getCompetitionChildren() }, + { + path: 'result', + element: , + children: getResultChildren() + }, { path: 'me', element: diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 825a6a0..2d5c6c5 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -21,6 +21,7 @@ export function Nav() {
  • Accueil
  • + @@ -41,6 +42,23 @@ function AffiliationMenu() { return
  • Demande d'affiliation
  • } +function CompMenu() { + const {is_authenticated} = useAuth() + + if (!is_authenticated) + return <> + + return
  • + +
      +
    • Inscription
    • +
    • Mes résultats
    • +
    +
  • +} + function ClubMenu() { const {is_authenticated, userinfo} = useAuth() @@ -99,4 +117,4 @@ function LoginMenu() { } -} \ No newline at end of file +} diff --git a/src/main/webapp/src/pages/result/DrawGraph.jsx b/src/main/webapp/src/pages/result/DrawGraph.jsx new file mode 100644 index 0000000..47a88ea --- /dev/null +++ b/src/main/webapp/src/pages/result/DrawGraph.jsx @@ -0,0 +1,252 @@ +import {useEffect, useRef} from "react"; + +const max_x = 500; +const size = 24; + +export function DrawGraph({root = []}) { + const canvasRef = useRef(null); + + function getBounds(root) { + let px = max_x; + let py; + let maxx, minx, miny, maxy + + function drawNode(tree, px, py) { + let death = tree.death() - 1 + + if (death === 0) { + if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0; + if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0; + } else { + if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0)) + miny = py - size * 2 * death - ((size * 1.5 / 2) | 0); + if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8; + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + maxx = px; + minx = px; + miny = py - (size * 1.5 / 2) | 0; + maxy = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + px = px - size * 2 - size * 8; + if (minx > px) minx = px; + + drawNode(node, px, py); + //graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny); + py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = maxx; + } + } else { + minx = 0; + maxx = 0; + miny = 0; + maxy = 0; + } + + return [minx, maxx, miny, maxy]; + } + + // Fonction pour dessiner du texte avec gestion de la taille + const printText = (ctx, s, x, y, width, height, lineG, lineD) => { + ctx.save(); + ctx.translate(x, y); + let tSize = 17; + let ratioX = height * 1.0 / 20.0; + ctx.font = "100 " + tSize + "px Arial"; + + let mw = width - (ratioX * 2) | 0; + if (ctx.measureText(s).width > mw) { + do { + tSize--; + ctx.font = tSize + "px Arial"; + } while (ctx.measureText(s).width > mw && tSize > 10); + + if (ctx.measureText(s).width > mw) { + let truncated = ""; + const words = s.split(" "); + for (const word of words) { + if (ctx.measureText(truncated + word).width >= mw) { + truncated += "..."; + break; + } else { + truncated += word + " "; + } + } + s = truncated; + } + } + + const text = ctx.measureText(s); + let dx = (width - text.width) / 2; + let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + ctx.fillText(s, dx, dy, width - dy); + ctx.restore(); + + ctx.beginPath(); + if (lineD) { + ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2); + ctx.lineTo(x + width, y + height / 2); + } + if (lineG) { + ctx.moveTo(x, y + height / 2); + ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2); + } + ctx.stroke(); + }; + + // Fonction pour afficher les scores + const printScores = (ctx, scores, px, py, scale) => { + ctx.save(); + ctx.translate(px - size * 2, py - size * scale); + ctx.font = "100 14px Arial"; + ctx.textBaseline = 'top'; + + for (let i = 0; i < scores.length; i++) { + const score = scores[i].s1 + "-" + scores[i].s2; + const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; + const text = ctx.measureText(score); + let dx = (size * 2 - text.width) / 2; + let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillStyle = '#ffffffdd'; + ctx.fillRect(dx, size * 2 * scale / div * i + dy, text.width, 14); + ctx.fillStyle = "#000000"; + ctx.fillText(score, dx, size * 2 * scale / div * i + dy, size * 2); + } + ctx.restore(); + }; + + // Fonction pour dessiner un nœud + const drawNode = (ctx, tree, px, py, max_y) => { + ctx.beginPath(); + ctx.moveTo(px, py); + ctx.lineTo(px - size, py); + ctx.stroke(); + + let death = tree.death() - 1; + let match = tree.data; + + if (death === 0) { + ctx.beginPath(); + ctx.moveTo(px - size, py + size); + ctx.lineTo(px - size, py - size); + ctx.moveTo(px - size, py + size); + ctx.lineTo(px - size * 2, py + size); + ctx.moveTo(px - size, py - size); + ctx.lineTo(px - size * 2, py - size); + ctx.stroke(); + + printScores(ctx, match.scores, px, py, 1); + ctx.fillStyle = "#FF0000"; + printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, + px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0), + size * 8, (size * 1.5 | 0), false, true); + + ctx.fillStyle = "#0000FF"; + printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, + px - size * 2 - size * 8, py + size - (size * 1.5 / 2 | 0), + size * 8, (size * 1.5 | 0), false, true); + + if (max_y.current < py + size + ((size * 1.5 / 2) | 0)) { + max_y.current = py + size + (size * 1.5 / 2 | 0); + } + } else { + ctx.beginPath(); + ctx.moveTo(px - size, py); + ctx.lineTo(px - size, py + size * 2 * death); + ctx.moveTo(px - size, py); + ctx.lineTo(px - size, py - size * 2 * death); + ctx.moveTo(px - size, py + size * 2 * death); + ctx.lineTo(px - size * 2, py + size * 2 * death); + ctx.moveTo(px - size, py - size * 2 * death); + ctx.lineTo(px - size * 2, py - size * 2 * death); + ctx.stroke(); + + printScores(ctx, match.scores, px, py, 1.5); + ctx.fillStyle = "#FF0000"; + printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, + px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0), + size * 8, (size * 1.5 | 0), true, true); + + ctx.fillStyle = "#0000FF"; + printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, + px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2 | 0), + size * 8, (size * 1.5 | 0), true, true); + + if (max_y.current < py + size * 2 * death + ((size * 1.5 / 2) | 0)) { + max_y.current = py + size * 2 * death + ((size * 1.5 / 2 | 0)); + } + } + + if (tree.left != null) { + drawNode(ctx, tree.left, px - size * 2 - size * 8, py - size * 2 * death, max_y); + } + if (tree.right != null) { + drawNode(ctx, tree.right, px - size * 2 - size * 8, py + size * 2 * death, max_y); + } + }; + + // Fonction pour déterminer le gagnant + const win = (scores) => { + let sum = 0; + for (const score of scores) { + if (score.s1 === -1000 || score.s2 === -1000) continue; + if (score.s1 > score.s2) sum++; + else if (score.s1 < score.s2) sum--; + } + return sum; + }; + + // Effet pour dessiner le graphique + useEffect(() => { + if (root.length === 0) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + const [minx, maxx, miny, maxy] = getBounds(root); + + canvas.width = maxx - minx; + canvas.height = maxy - miny; + ctx.translate(-minx, -miny); + ctx.fillStyle = "#000000"; + ctx.lineWidth = 2; + ctx.strokeStyle = "#000000"; + + let px = maxx; + let py; + const max_y = {current: 0}; + + py = (size * 2 * root[0].death() + (((size * 1.5 / 2) | 0) + size) * root[0].death()) * 2; + max_y.current = py + (size * 1.5 / 2 | 0); + for (const node of root) { + let win_name = ""; + if (node.data.end) { + win_name = win(node.data.scores) > 0 + ? (node.data.c1FullName === null ? "???" : node.data.c1FullName) + : (node.data.c2FullName === null ? "???" : node.data.c2FullName); + } + + ctx.fillStyle = "#18A918"; + printText(ctx, win_name, + px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0), + size * 8, (size * 1.5 | 0), true, false); + + px = px - size * 2 - size * 8; + drawNode(ctx, node, px, py, max_y); + py = max_y.current + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = maxx; + } + }, [root]); + + return ; +} diff --git a/src/main/webapp/src/pages/result/ResultList.jsx b/src/main/webapp/src/pages/result/ResultList.jsx new file mode 100644 index 0000000..6baccb6 --- /dev/null +++ b/src/main/webapp/src/pages/result/ResultList.jsx @@ -0,0 +1,49 @@ +import {useNavigate} from "react-router-dom"; +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"; + +export function ResultList() { + const navigate = useNavigate(); + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/result/list`, setLoading, 1) + + return <> +
    +
    + {data + ? + : error + ? + : + } +
    +
    + +} + +function MakeCentralPanel({data, navigate}) { + + return <> +
    +

    Compétition:

    +
    + {data.sort((a, b) => new Date(b[2].split('T')[0]) - new Date(a[2].split('T')[0])).map((o) => ( +
  • navigate(`${o[0]}`)}>{o[1]}
  • ))} +
    +
    + +} + +function Def() { + return
    +
  • +
  • +
  • +
  • +
  • +
    +} diff --git a/src/main/webapp/src/pages/result/ResultRoot.jsx b/src/main/webapp/src/pages/result/ResultRoot.jsx new file mode 100644 index 0000000..e95ff48 --- /dev/null +++ b/src/main/webapp/src/pages/result/ResultRoot.jsx @@ -0,0 +1,26 @@ +import {LoadingProvider} from "../../hooks/useLoading.jsx"; +import {Outlet} from "react-router-dom"; +import {ResultList} from "./ResultList.jsx"; +import {ResultView} from "./ResultView.jsx"; + +export function ResultRoot() { + return <> +

    Résultat

    + + + + +} + +export function getResultChildren() { + return [ + { + path: '', + element: + }, + { + path: ':uuid', + element: + }, + ] +} diff --git a/src/main/webapp/src/pages/result/ResultView.jsx b/src/main/webapp/src/pages/result/ResultView.jsx new file mode 100644 index 0000000..9efe245 --- /dev/null +++ b/src/main/webapp/src/pages/result/ResultView.jsx @@ -0,0 +1,347 @@ +import {useNavigate, useParams} from "react-router-dom"; +import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; +import {useFetch} from "../../hooks/useFetch.js"; +import {AxiosError} from "../../components/AxiosError.jsx"; +import {ThreeDots} from "react-loader-spinner"; +import {useEffect, useState} from "react"; +import {DrawGraph} from "./DrawGraph.jsx"; + +function CupImg() { + return +} + +export function ResultView() { + const {uuid} = useParams() + const navigate = useNavigate(); + const [resultShow, setResultShow] = useState(null) + + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/result/${uuid}`, setLoading, 1) + + return <> + + + {data ? + : error + ? + : } + +
    +
    + {resultShow && resultShow.type !== undefined && + || resultShow && resultShow === "club" && + || resultShow && resultShow === "comb" && } +
    +
    + +} +// || resultShow && resultShow === "club_all" && + +function MenuBar({data, resultShow, setResultShow}) { + return + + /* +
  • + setResultShow("club_all")}>Clubs +
  • + */ +} + +function scoreToString(score) { + const scorePrint = (s1) => { + switch (s1) { + case -997: + return "disc."; + case -998: + return "abs."; + case -999: + return "for."; + case -1000: + return ""; + default: + return String(s1); + } + } + + return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); +} + +function BuildMatchArray({matchs}) { + return <> + + + + + + + + + + + + {matchs.map((match, idx) => + + + + + + )} + +
    RougeScoresBleu
    {match.red}{match.red_w ? : ""}{scoreToString(match.score)}{match.blue_w ? : ""}{match.blue}
    + +} + +function BuildRankArray({rankArray}) { + return <> + + + + + + + + + + + + + {rankArray.map((row, idx) => + + + + + + + )} + +
    PlaceNomVictoireRatioPoints marquésPoints reçus
    {row.rank}{row.name}{row.win}{row.pointRate.toFixed(3)}{row.pointMake}{row.pointTake}
    + +} + +function BuildTree({treeData}) { + class TreeNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + } + + death() { + let dg = 0; + let dd = 0; + + if (this.right != null) + dg = this.right.death(); + + if (this.left != null) + dg = this.left.death(); + + return 1 + Math.max(dg, dd); + } + } + + function parseTree(data_in) { + if (data_in?.data == null) + return null; + + let node = new TreeNode(data_in.data); + node.left = parseTree(data_in?.left); + node.right = parseTree(data_in?.right); + + return node; + } + + function initTree(data_in) { + let out = []; + for (const din of data_in) { + out.push(parseTree(din)); + } + return out; + } + + return +} + +function PouleResult({data}) { + const [type, setType] = useState(data.type === 3 ? 1 : data.type) + + useEffect(() => { + setType(data.type === 3 ? 1 : data.type) + }, [data]); + + return <> + {data.type === 3 && <> + + } + + {type === 1 && <> + {Object.keys(data.matchs).map(p =>
    + {Object.keys(data.matchs).length > 1 &&

    Poule {p}

    } + + +
    )} + } + + {type === 2 && <> + + } + + +} + +function ClubResult({uuid}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/result/${uuid}/club`, setLoading, 1) + + return <> + {data ? <> +

    Info :

    +
      +
    • Nom : {data.name}
    • +
    • Nombre d'inscris : {data.nb_insc}
    • +
    +

    Statistique :

    +
      +
    • Nombre de match disputé : {data.nb_match}
    • +
    • Nombre de victoires : {data.match_w}
    • +
    • Ratio de victoires moyen : {data.ratioVictoire.toFixed(3)}
    • +
    • Points marqués : {data.pointMake}
    • +
    • Points reçus : {data.pointTake}
    • +
    • Ratio de points moyen : {data.ratioPoint.toFixed(3)}
    • +
    + +

    Liste des membres :

    + + + + + + + + + + + + + + + + {data.combs.map((comb, idx) => + + + + + + + + + )} + +
    CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points
    {comb.cat}{comb.name}{comb.w}{comb.l}{comb.ratioVictoire.toFixed(3)}{comb.pointMake}{comb.pointTake}{comb.ratioPoint.toFixed(3)}
    + + + : error + ? + : + } + +} + +/*function ClubAllResult({uuid}) { + return
    + +}*/ + +function CombResult({uuid}) { + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1) + + return <> + {data ? <> +

    Statistique :

    +
      +
    • Nombre d'inscris : {data.nb_insc}
    • +
    • Nombre de match disputé : {data.tt_match}
    • +
    • Points marqués : {data.point}
    • +
    + +

    Liste des combattants :

    + + + + + + + + + + + + + + + + + {data.combs.map((comb, idx) => + + + + + + + + + + )} + +
    CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points
    {comb.cat}{comb.club}{comb.name}{comb.w}{comb.l}{comb.ratioVictoire.toFixed(3)}{comb.pointMake}{comb.pointTake}{comb.ratioPoint.toFixed(3)}
    + + : error + ? + : + } + +} + +function Def() { + return
    +
  • +
  • +
  • +
  • +
  • +
    +} -- 2.49.0 From baf57c346446fea8e9aaeda96b4bf2bc31d3de89 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 3 Sep 2025 19:53:19 +0200 Subject: [PATCH 02/59] fix: log message length --- src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java index b3d170c..4d1dd9d 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/LogModel.java @@ -30,8 +30,10 @@ public class LogModel { Long target_id; + @Column(columnDefinition = "TEXT") String target_name; + @Column(columnDefinition = "TEXT") String message; public enum ActionType { -- 2.49.0 From ac6563ac9528a6296f46d32ae6af675605cdc4e4 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 3 Sep 2025 21:35:36 +0200 Subject: [PATCH 03/59] fix: club order --- src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java | 4 +++- src/main/webapp/src/pages/admin/club/ClubPage.jsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index d4dcf09..7048e4f 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -29,6 +29,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import java.net.URISyntaxException; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; @@ -69,7 +70,8 @@ public class ClubEndpoints { @APIResponse(responseCode = "500", description = "Erreur interne du serveur") }) public Uni> getAll() { - return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList()); + return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).sorted( + Comparator.comparing(SimpleClubModel::getName)).toList()); } @GET diff --git a/src/main/webapp/src/pages/admin/club/ClubPage.jsx b/src/main/webapp/src/pages/admin/club/ClubPage.jsx index 0e4b4dd..5a6768a 100644 --- a/src/main/webapp/src/pages/admin/club/ClubPage.jsx +++ b/src/main/webapp/src/pages/admin/club/ClubPage.jsx @@ -172,6 +172,7 @@ function InformationForm({data}) { export function BureauCard({clubData}) { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1) + const navigate = useNavigate(); return <>
    @@ -179,7 +180,8 @@ export function BureauCard({clubData}) {
      {data && data.map((d, index) => { - return
      + return
      navigate(`/admin/member/${d.id}`)}>
      {d.role}
      {d.lname} {d.fname}
      })} @@ -188,4 +190,4 @@ export function BureauCard({clubData}) {
      {error && } -} \ No newline at end of file +} -- 2.49.0 From b84e10de443e73fa32729c71930e020ff2c32500 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 3 Sep 2025 21:46:26 +0200 Subject: [PATCH 04/59] feat: keep log --- .gitea/workflows/deploy_in_prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy_in_prod.yml b/.gitea/workflows/deploy_in_prod.yml index d35802c..464262f 100644 --- a/.gitea/workflows/deploy_in_prod.yml +++ b/.gitea/workflows/deploy_in_prod.yml @@ -76,6 +76,7 @@ jobs: key: ${{ secrets.SSH_KEY }} script: | cd ${{ secrets.TARGET_DIR }} + docker logs ffsaf > "log/ffsaf_logs_$(date +"%Y-%m-%d_%H-%M-%S").log" 2>&1 docker stop ffsaf docker rm ffsaf docker compose up --build -d ffsaf -- 2.49.0 From 4262845074627f6cde63b2b1f1ad91026efa4442 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 5 Sep 2025 20:31:10 +0200 Subject: [PATCH 05/59] fix: typo --- .../src/components/Club/HoraireEditor.jsx | 4 ++-- .../src/components/Club/LocationEditor.jsx | 2 +- src/main/webapp/src/components/Nav.jsx | 4 ++-- src/main/webapp/src/pages/Homepage.jsx | 24 +++++++++---------- src/main/webapp/src/pages/club/ClubRoot.jsx | 2 +- .../src/pages/club/club/AffiliationCard.jsx | 11 ++++----- .../webapp/src/pages/club/club/MyClubPage.jsx | 6 ++--- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/main/webapp/src/components/Club/HoraireEditor.jsx b/src/main/webapp/src/components/Club/HoraireEditor.jsx index 3b6dd25..b1be143 100644 --- a/src/main/webapp/src/components/Club/HoraireEditor.jsx +++ b/src/main/webapp/src/components/Club/HoraireEditor.jsx @@ -38,7 +38,7 @@ export function HoraireEditor({data}) { return
      - Horaires d'entrainements + Horaires d'entraînements
        {state.map((d, index) => { return
        @@ -92,4 +92,4 @@ export function HoraireEditor({data}) {
      -} \ No newline at end of file +} diff --git a/src/main/webapp/src/components/Club/LocationEditor.jsx b/src/main/webapp/src/components/Club/LocationEditor.jsx index 9369078..2308106 100644 --- a/src/main/webapp/src/components/Club/LocationEditor.jsx +++ b/src/main/webapp/src/components/Club/LocationEditor.jsx @@ -42,7 +42,7 @@ export function LocationEditor({data, setModal, sendData}) { return
      - Lieux d'entrainements + Lieux d'entraînements
        {state.map((d, index) => { return
        diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 2d5c6c5..1230802 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -72,7 +72,7 @@ function ClubMenu() {
        • Mon club
        • -
        • Member
        • +
        • Membres
        } @@ -88,7 +88,7 @@ function AdminMenu() { Administration
        -
      • Member
      • +
      • Membres
      • Club
      • Statistiques
      diff --git a/src/main/webapp/src/pages/Homepage.jsx b/src/main/webapp/src/pages/Homepage.jsx index c465ad1..912e271 100644 --- a/src/main/webapp/src/pages/Homepage.jsx +++ b/src/main/webapp/src/pages/Homepage.jsx @@ -5,22 +5,22 @@ export const Home = () => { return <>
      -

      Bienvenu sur l'intranet de Fédération Française de Soft Armored Fighting

      +

      Bienvenue sur l’intranet de la Fédération France Soft Armored Fighting

      -

      Pour les combatants

      +

      Pour les licenciés

      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 qu'en consultée vos résultats sous réserve - que le club organisateur les ait renseignés.
      + 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.

      - Lors de votre première inscription, vous réservez un email contenant vos - informations d'identification sur ce site, ce mail sera envoyé une fois votre inscription validée par nos soins. + 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.

      @@ -30,12 +30,12 @@ export const Home = () => {

      Pour les clubs

      - C'est ici que vous pouvez faire l'inscription de vos membres à la fédération, que vous pouvez demander où renouveler votre - demande d'affiliation, renseigné vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur le - site ffsaf.fr.
      + 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.
      Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi - que d'un publié les résultats.

      - Vous n'étes pas encore affilié à la fédération ? Vous pouvez faire une demande d'affiliation en cliquant içi. + que d'enregistrer les résultats.

      + Vous n'êtes pas encore affilié à la fédération ? Cliquez içi pour faire votre première demande.

      @@ -55,4 +55,4 @@ export const Home = () => { }}>
    -}; \ No newline at end of file +}; diff --git a/src/main/webapp/src/pages/club/ClubRoot.jsx b/src/main/webapp/src/pages/club/ClubRoot.jsx index 70cccf8..eab9bee 100644 --- a/src/main/webapp/src/pages/club/ClubRoot.jsx +++ b/src/main/webapp/src/pages/club/ClubRoot.jsx @@ -23,7 +23,7 @@ export function ClubRoot() { return <>
    -

    Espace club

    {club}

    +

    Club: {club}

    diff --git a/src/main/webapp/src/pages/club/club/AffiliationCard.jsx b/src/main/webapp/src/pages/club/club/AffiliationCard.jsx index 168963a..f6de7e3 100644 --- a/src/main/webapp/src/pages/club/club/AffiliationCard.jsx +++ b/src/main/webapp/src/pages/club/club/AffiliationCard.jsx @@ -1,12 +1,11 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useFetch} from "../../../hooks/useFetch.js"; -import {useEffect, useReducer, useState} from "react"; +import {useState} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faEye, faFilePdf, faPen} from "@fortawesome/free-solid-svg-icons"; +import {faEye, faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {AxiosError} from "../../../components/AxiosError.jsx"; -import {apiAxios, getSaison} from "../../../utils/Tools.js"; +import {apiAxios} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; -import {SimpleReducer} from "../../../utils/SimpleReducer.jsx"; import {useNavigate} from "react-router-dom"; const vite_url = import.meta.env.VITE_URL; @@ -42,8 +41,8 @@ export function AffiliationCard({clubData}) {
diff --git a/src/main/webapp/src/pages/club/club/MyClubPage.jsx b/src/main/webapp/src/pages/club/club/MyClubPage.jsx index 2fd2af2..d8e0fb6 100644 --- a/src/main/webapp/src/pages/club/club/MyClubPage.jsx +++ b/src/main/webapp/src/pages/club/club/MyClubPage.jsx @@ -1,9 +1,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 {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx"; import {AffiliationCard, BureauCard} from "./AffiliationCard.jsx"; import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx"; @@ -22,7 +20,7 @@ export function MyClubPage() { const {data, error} = useFetch(`/club/me`, setLoading, 1) return <> -

Mon club

+

Données administratives

{data ?
@@ -91,7 +89,7 @@ function InformationForm({data}) {
-- 2.49.0 From 7625da1d4bbf6899633141ab5c77f908fb496f90 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 5 Nov 2025 21:18:29 +0100 Subject: [PATCH 06/59] feat: add aff req log detail fix: membre import null email filter --- .../ffsaf/data/model/MembreModel.java | 1 + .../domain/service/AffiliationService.java | 13 +++++++ .../ffsaf/domain/service/MembreService.java | 14 ++++++- .../rest/from/AffiliationRequestSaveForm.java | 37 ++++++++++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java index b39db64..41642d1 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java @@ -20,6 +20,7 @@ import java.util.List; @RegisterForReflection @Entity +@ToString @Table(name = "membre") public class MembreModel implements LoggableModel { diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 70ba494..a2b79a8 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -22,6 +22,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.hibernate.reactive.mutiny.Mutiny; +import org.jboss.logging.Logger; import java.util.ArrayList; import java.util.List; @@ -30,6 +31,7 @@ import java.util.stream.Stream; @WithSession @ApplicationScoped public class AffiliationService { + private static final Logger LOGGER = Logger.getLogger(AffiliationService.class); @Inject CombRepository combRepository; @@ -146,6 +148,9 @@ public class AffiliationService { } public Uni save(AffiliationRequestForm form) { + LOGGER.debug("Affiliation Request Created"); + LOGGER.debug(form.toString()); + // noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher return pre_save(form, true) .chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model))) @@ -169,6 +174,9 @@ public class AffiliationService { } public Uni saveAdmin(AffiliationRequestSaveForm form) { + LOGGER.debug("Affiliation Request Saved"); + LOGGER.debug(form.toString()); + return repositoryRequest.findById(form.getId()) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .map(model -> { @@ -273,6 +281,9 @@ public class AffiliationService { } public Uni accept(AffiliationRequestSaveForm form) { + LOGGER.debug("Affiliation Request Accepted"); + LOGGER.debug(form.toString()); + return repositoryRequest.findById(form.getId()) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .chain(req -> @@ -298,6 +309,7 @@ public class AffiliationService { } private Uni acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) { + LOGGER.debug("New Club Accepted"); return Uni.createFrom().nullItem() .chain(() -> { ClubModel club = new ClubModel(); @@ -336,6 +348,7 @@ public class AffiliationService { } private Uni acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) { + LOGGER.debug("Old Club Accepted"); return Uni.createFrom().nullItem() .chain(() -> { club.setName(form.getName()); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index d4850a6..2dc7e7a 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -197,6 +197,11 @@ public class MembreService { return Uni.createFrom().nullItem(); AtomicReference clubModel = new AtomicReference<>(); + LOGGER.debugf("Membre import (size=%d)", data2.size()); + for (SimpleMembreInOutData simpleMembreInOutData : data2) { + LOGGER.debugf("-> %s", simpleMembreInOutData.toString()); + } + return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> { clubModel.set(membreModel.getClub()); @@ -205,13 +210,15 @@ public class MembreService { return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3", data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(), data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(), - data2.stream().map(SimpleMembreInOutData::getEmail).filter(Objects::nonNull).toList()); + data2.stream().map(SimpleMembreInOutData::getEmail).filter(o -> o != null && !o.isBlank()).toList()); }) .call(Unchecked.function(membres -> { for (MembreModel membreModel : membres) { - if (!Objects.equals(membreModel.getClub(), clubModel.get())) + 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"); + } } Uni uniResult = Uni.createFrom().voidItem(); for (SimpleMembreInOutData dataIn : data2) { @@ -229,12 +236,14 @@ public class MembreService { if (model.getEmail() != null) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { + LOGGER.info("Similar membres found: " + model); throw new DBadRequestException("Email déja utiliser"); } if (StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { + LOGGER.info("Similar membres found: " + model); throw new DBadRequestException("Email déja utiliser"); } } @@ -244,6 +253,7 @@ public class MembreService { if ((!add && StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3) || (!add && StringSimilarity.similarity( 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."); } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java index 409fd45..36f62e9 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java @@ -4,12 +4,10 @@ import fr.titionfire.ffsaf.utils.RoleAsso; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.core.MediaType; import lombok.Getter; -import lombok.ToString; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; @Getter -@ToString public class AffiliationRequestSaveForm { @Schema(description = "L'identifiant de l'affiliation.", example = "1", required = true) @FormParam("id") @@ -171,4 +169,39 @@ public class AffiliationRequestSaveForm { } } } + + @Override + public String toString() { + return "AffiliationRequestSaveForm{" + + "id=" + id + + ", name='" + name + '\'' + + ", siret=" + siret + + ", rna='" + rna + '\'' + + ", address='" + address + '\'' + + ", contact='" + contact + '\'' + + ", status_len=" + status.length + + ", logo_len=" + logo.length + + ", m1_mode=" + m1_mode + + ", m1_role=" + m1_role + + ", m1_lincence='" + m1_lincence + '\'' + + ", m1_lname='" + m1_lname + '\'' + + ", m1_fname='" + m1_fname + '\'' + + ", m1_email='" + m1_email + '\'' + + ", m1_email_mode=" + m1_email_mode + + ", m2_mode=" + m2_mode + + ", m2_role=" + m2_role + + ", m2_lincence='" + m2_lincence + '\'' + + ", m2_lname='" + m2_lname + '\'' + + ", m2_fname='" + m2_fname + '\'' + + ", m2_email='" + m2_email + '\'' + + ", m2_email_mode=" + m2_email_mode + + ", m3_mode=" + m3_mode + + ", m3_role=" + m3_role + + ", m3_lincence='" + m3_lincence + '\'' + + ", m3_lname='" + m3_lname + '\'' + + ", m3_fname='" + m3_fname + '\'' + + ", m3_email='" + m3_email + '\'' + + ", m3_email_mode=" + m3_email_mode + + '}'; + } } -- 2.49.0 From 77d66813c7c61ab03cb2ed438b68a2eb7088d006 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 5 Nov 2025 21:43:05 +0100 Subject: [PATCH 07/59] fix: log detail --- .../ffsaf/data/model/MembreModel.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java index 41642d1..845246e 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java @@ -20,7 +20,6 @@ import java.util.List; @RegisterForReflection @Entity -@ToString @Table(name = "membre") public class MembreModel implements LoggableModel { @@ -83,4 +82,23 @@ public class MembreModel implements LoggableModel { public LogModel.ObjectType getObjectType() { return LogModel.ObjectType.Membre; } + + @Override + public String toString() { + return "MembreModel{" + + "id=" + id + + ", userId='" + userId + '\'' + + ", lname='" + lname + '\'' + + ", fname='" + fname + '\'' + + ", categorie=" + categorie + + ", genre=" + genre + + ", licence=" + licence + + ", country='" + country + '\'' + + ", birth_date=" + birth_date + + ", email='" + email + '\'' + + ", role=" + role + + ", grade_arbitrage=" + grade_arbitrage + + ", licences=" + licences.size() + + '}'; + } } -- 2.49.0 From 9e28356f2cea778d1c0d3d246742ac4f2e0d3ee6 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 5 Nov 2025 22:06:36 +0100 Subject: [PATCH 08/59] fix: log detail --- src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java index 845246e..218c8c9 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MembreModel.java @@ -98,7 +98,6 @@ public class MembreModel implements LoggableModel { ", email='" + email + '\'' + ", role=" + role + ", grade_arbitrage=" + grade_arbitrage + - ", licences=" + licences.size() + '}'; } } -- 2.49.0 From cc4a3e4e06c32a50eb1329c2b6c2e05ba622f241 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 7 Nov 2025 15:30:56 +0100 Subject: [PATCH 09/59] fix: null email on import --- .../ffsaf/domain/service/AffiliationService.java | 6 ++++-- .../titionfire/ffsaf/domain/service/LicenceService.java | 6 +++++- .../fr/titionfire/ffsaf/domain/service/MembreService.java | 8 +++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index a2b79a8..3bafd63 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -267,7 +267,9 @@ public class AffiliationService { }).call(m -> Panache.withTransaction(() -> combRepository.persist(m))); } }) - .call(m -> ((m.getUserId() == null) ? keycloakService.initCompte(m.getId()) : + .call(m -> ((m.getUserId() == null) ? keycloakService.initCompte(m.getId()) + .onFailure().invoke(t -> LOGGER.warnf("Failed to init account: %s", t.getMessage())).onFailure() + .recoverWithNull() : keycloakService.setClubGroupMembre(m, club).map(__ -> m.getUserId())) .call(userId -> keycloakService.setAutoRoleMembre(userId, m.getRole(), m.getGrade_arbitrage())) .call(userId -> keycloakService.setEmail(userId, m.getEmail()))) @@ -275,7 +277,7 @@ public class AffiliationService { .call(l1 -> l1 != null && l1.stream().anyMatch(l -> l.getSaison() == saison) ? Uni.createFrom().nullItem() : Panache.withTransaction(() -> licenceRepository.persist( - new LicenceModel(null, m, club.getId(), saison, null, true, false))) + new LicenceModel(null, m, club.getId(), saison, null, true, false))) .call(licenceModel -> ls.logA(LogModel.ActionType.ADD, m.getObjectName(), licenceModel)))); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java index 2269249..751df30 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java @@ -18,6 +18,7 @@ import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.hibernate.reactive.mutiny.Mutiny; +import org.jboss.logging.Logger; import java.util.List; import java.util.function.Consumer; @@ -26,6 +27,7 @@ import java.util.function.Function; @WithSession @ApplicationScoped public class LicenceService { + private static final Logger LOGGER = Logger.getLogger(LicenceService.class); @Inject LicenceRepository repository; @@ -125,7 +127,9 @@ public class LicenceService { .chain(() -> combRepository.persist(membreModel)) : Uni.createFrom().nullItem()) .call(__ -> (membreModel.getUserId() == null) ? - keycloakService.initCompte(membreModel.getId()) + keycloakService.initCompte(membreModel.getId()).onFailure() + .invoke(t -> LOGGER.infof("Failed to init account: %s", t.getMessage())).onFailure() + .recoverWithNull() : Uni.createFrom().nullItem()); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 2dc7e7a..781cf95 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -210,11 +210,12 @@ public class MembreService { return repository.list("licence IN ?1 OR LOWER(lname || ' ' || fname) IN ?2 OR email IN ?3", data2.stream().map(SimpleMembreInOutData::getLicence).filter(Objects::nonNull).toList(), data2.stream().map(o -> (o.getNom() + " " + o.getPrenom()).toLowerCase()).toList(), - data2.stream().map(SimpleMembreInOutData::getEmail).filter(o -> o != null && !o.isBlank()).toList()); + data2.stream().map(SimpleMembreInOutData::getEmail).filter(o -> o != null && !o.isBlank()) + .toList()); }) .call(Unchecked.function(membres -> { for (MembreModel membreModel : membres) { - if (!Objects.equals(membreModel.getClub(), clubModel.get())){ + 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"); @@ -225,7 +226,8 @@ public class MembreService { MembreModel model = membres.stream() .filter(m -> Objects.equals(m.getLicence(), dataIn.getLicence()) || m.getLname() .equals(dataIn.getNom()) && m.getFname().equals(dataIn.getPrenom()) || - Objects.equals(m.getFname(), dataIn.getEmail())).findFirst() + (dataIn.getEmail() != null && !dataIn.getEmail().isBlank() && Objects.equals( + m.getFname(), dataIn.getEmail()))).findFirst() .orElseGet(() -> { MembreModel mm = new MembreModel(); mm.setClub(clubModel.get()); -- 2.49.0 From 7e80703c0439a4666da33eaef2028b50eeea20ee Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 7 Nov 2025 15:31:25 +0100 Subject: [PATCH 10/59] feat: add re-login message --- src/main/webapp/src/App.jsx | 45 ++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index 1cee2e9..c6b2712 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -1,11 +1,11 @@ import {useEffect, useRef} from 'react' import {Nav} from "./components/Nav.jsx"; -import {createBrowserRouter, Outlet, RouterProvider, useRouteError} from "react-router-dom"; +import {createBrowserRouter, Outlet, RouterProvider, useLocation, useRouteError} from "react-router-dom"; import {Home} from "./pages/Homepage.jsx"; import {AdminRoot, getAdminChildren} from "./pages/admin/AdminRoot.jsx"; import {AuthCallback} from "./components/auhCallback.jsx"; -import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx"; -import {check_validity} from "./utils/auth.js"; +import {KeycloakContextProvider, useAuth, useAuthDispatch} from "./hooks/useAuth.jsx"; +import {check_validity, login} from "./utils/auth.js"; import {ToastContainer} from "react-toastify"; import './App.css' @@ -14,6 +14,7 @@ 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"; const router = createBrowserRouter([ @@ -119,6 +120,44 @@ function Root() { theme="light" transition: Flip /> + +
+ +} + +function ReAuthMsg() { + const {is_authenticated} = useAuth() + const location = useLocation() + + const notAuthPaths = [ + /^\/$/s, + /^\/affiliation(\/)?$/s, + /^\/complete\/auth.*$/s + ] + + console.log(location.pathname, notAuthPaths.some(r => r.test(location.pathname))) + + if (is_authenticated || notAuthPaths.some(r => r.test(location.pathname))) + return <> + return <> + } -- 2.49.0 From 80fef98e07ab241e3ba25a9700fed0bad60a6fc6 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 7 Nov 2025 15:53:39 +0100 Subject: [PATCH 11/59] feat: add email tooltip --- src/main/webapp/src/components/MemberCustomFiels.jsx | 7 ++++--- .../src/pages/admin/affiliation/AffiliationReqPage.jsx | 3 +-- src/main/webapp/src/pages/club/member/InformationForm.jsx | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/src/components/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx index 7c0a00c..0fd9602 100644 --- a/src/main/webapp/src/components/MemberCustomFiels.jsx +++ b/src/main/webapp/src/components/MemberCustomFiels.jsx @@ -88,13 +88,14 @@ export function CountryList({name, text, value, values = undefined, disabled = f
} -export function TextField({name, text, value, placeholder, type = "text", disabled = false, required = true}) { - return
-
+export function TextField({name, text, value, placeholder, type = "text", disabled = false, required = true, ttip = null}) { + return
+
{text}
+ {ttip}
} diff --git a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx index 9c8a98e..cb292bb 100644 --- a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx +++ b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx @@ -8,7 +8,6 @@ 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 {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; const vite_url = import.meta.env.VITE_URL; @@ -166,7 +165,7 @@ function Content({data, refresh}) {
Demande d'affiliation
- {data.club &&
Ce club a déjà ete affilier (affiliation n°{data.club_no_aff})
} + {data.club &&
Ce club a déjà été affilié (affiliation n°{data.club_no_aff})
}

Saison {data.saison}-{data.saison + 1}

diff --git a/src/main/webapp/src/pages/club/member/InformationForm.jsx b/src/main/webapp/src/pages/club/member/InformationForm.jsx index a6231ea..9446f96 100644 --- a/src/main/webapp/src/pages/club/member/InformationForm.jsx +++ b/src/main/webapp/src/pages/club/member/InformationForm.jsx @@ -49,7 +49,9 @@ export function InformationForm({data}) { + type="email" ttip={L'email sert à la création de compte pour se connecter au site et doit être unique.
+ Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+@exemple.com'}.
+ Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com
}/> -- 2.49.0 From b320d7db372003cb54be08ba0d5df9ed2a0ace94 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 7 Nov 2025 15:57:36 +0100 Subject: [PATCH 12/59] fix: affiliation ok login msg --- src/main/webapp/src/App.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index c6b2712..de14cca 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -132,6 +132,7 @@ function ReAuthMsg() { const notAuthPaths = [ /^\/$/s, /^\/affiliation(\/)?$/s, + /^\/affiliation\/ok(\/)?$/s, /^\/complete\/auth.*$/s ] -- 2.49.0 From ee476cd0e286bb00fbdc42a36de32299066e370e Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 7 Nov 2025 16:13:37 +0100 Subject: [PATCH 13/59] feat: remove saison selection on aff req --- .../rest/from/AffiliationRequestForm.java | 2 +- src/main/webapp/src/pages/DemandeAff.jsx | 34 ++++++------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java index 961033c..9dfb985 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -11,7 +11,7 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.resteasy.reactive.PartType; @Getter -@ToString +@ToString(exclude = {"status", "logo"}) public class AffiliationRequestForm { @Schema(description = "L'identifiant de l'affiliation. (null si nouvelle demande d'affiliation)") @FormParam("id") diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index ccfa4fd..d0a2953 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -20,11 +20,11 @@ function reconstruireAdresse(infos) { console.log(infos); let adresseReconstruite = ""; - if(infos.numero_voie === null){ + if (infos.numero_voie === null) { if (infos.complement_adresse) { adresseReconstruite += formatAdresse(infos.complement_adresse) + ', '; } - }else{ + } else { adresseReconstruite += infos.numero_voie + ' '; } @@ -46,6 +46,13 @@ function reconstruireAdresse(infos) { return adresseReconstruite; } +function getSaisonToAff(currentDate = new Date()) { + if (currentDate.getMonth() >= 7) { //aout et plus + return currentDate.getFullYear() + } else { + return currentDate.getFullYear() - 1 + } +} export function DemandeAff() { const {hash} = useLocation(); @@ -145,7 +152,7 @@ export function DemandeAff() { } return
-

Demande d'affiliation

+

Demande d'affiliation {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}

L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année suivante.

Pour s’affilier, une association sportive doit réunir les conditions suivantes : @@ -216,7 +223,6 @@ function AssoInfo({initData, needFile}) { const [rna, setRna] = useState(initData.rna ? initData.rna : "") const [rnaEnable, setRnaEnable] = useState(false) const [adresse, setAdresse] = useState(initData.address ? initData.address : "") - const [saison, setSaison] = useState(initData.saison ? initData.saison : getSaison()) const [contact, setContact] = useState(initData.contact ? initData.contact : "") const fetchSiret = () => { @@ -245,26 +251,8 @@ function AssoInfo({initData, needFile}) { setAdresse(reconstruireAdresse(data2.etablissement_siege)) }) } - - const currentSaison = getSaison(); - return <> -
-
- setSaison(Number(e.target.value))}/> - {currentSaison + "-" + (currentSaison + 1)} -
- OU -
- setSaison(Number(e.target.value))}/> - {(currentSaison + 1) + "-" + (currentSaison + 2)} -
-
+
Nom de l'association* -- 2.49.0 From fccea5bf6aa2ed8ed1ad589417ccf69bfa173533 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 7 Nov 2025 16:19:49 +0100 Subject: [PATCH 14/59] fix: aff renew select length --- src/main/webapp/src/App.jsx | 2 -- src/main/webapp/src/pages/club/club/AffiliationCard.jsx | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index de14cca..0d966e0 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -136,8 +136,6 @@ function ReAuthMsg() { /^\/complete\/auth.*$/s ] - console.log(location.pathname, notAuthPaths.some(r => r.test(location.pathname))) - if (is_authenticated || notAuthPaths.some(r => r.test(location.pathname))) return <> return <> diff --git a/src/main/webapp/src/pages/club/club/AffiliationCard.jsx b/src/main/webapp/src/pages/club/club/AffiliationCard.jsx index f6de7e3..33439d6 100644 --- a/src/main/webapp/src/pages/club/club/AffiliationCard.jsx +++ b/src/main/webapp/src/pages/club/club/AffiliationCard.jsx @@ -139,10 +139,10 @@ function ModalContent2({clubData, data}) { } } - if (list.length !== 3) { - toast.error("Il faut sélectionner 3 membres pour renouveler l'affiliation") - return + while (list.length < 3) { + list.push(-1) } + apiAxios.get(`/club/renew/${clubData.id}?m1=${list[0]}&m2=${list[1]}&m3=${list[2]}`).then(data => { navigate('/affiliation#d' + encodeURI(JSON.stringify(data.data))) }) -- 2.49.0 From d9fc68298ce5b758d1dd197ae9d1e77f941ccae1 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sat, 8 Nov 2025 18:44:35 +0100 Subject: [PATCH 15/59] feat: allow empty mail --- .../fr/titionfire/ffsaf/domain/service/MembreService.java | 4 ++-- src/main/webapp/src/utils/auth.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 781cf95..51f2ac9 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -331,7 +331,7 @@ public class MembreService { return update(repository.findById(id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { - if (c > 0) + if (c > 0 && !membre.getEmail().isBlank()) throw new DBadRequestException("Email déjà utiliser"); }))) .chain(membreModel -> clubRepository.findById(membre.getClub()) @@ -353,7 +353,7 @@ public class MembreService { return update(repository.findById(id) .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { - if (c > 0) + if (c > 0 && !membre.getEmail().isBlank()) throw new DBadRequestException("Email déjà utiliser"); }))) .invoke(Unchecked.consumer(membreModel -> { diff --git a/src/main/webapp/src/utils/auth.js b/src/main/webapp/src/utils/auth.js index 0b12240..0e5c3d1 100644 --- a/src/main/webapp/src/utils/auth.js +++ b/src/main/webapp/src/utils/auth.js @@ -9,8 +9,11 @@ export function check_validity(online_callback = () => { axios.get(`${vite_url}/api/auth/userinfo`).then(data => { online_callback({state: true, userinfo: data.data}); }) + }else{ + online_callback({state: false}); } }).catch(() => { + console.log("=> Not authenticated"); online_callback({state: false}); }) } @@ -32,4 +35,4 @@ export function login_redirect() { export function logout() { window.location.href = `${vite_url}/api/logout`; -} \ No newline at end of file +} -- 2.49.0 From 61a4af6ff1c2bc0938f84a9dcc9c8a80cbcc0a7f Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Sat, 8 Nov 2025 19:08:41 +0100 Subject: [PATCH 16/59] fix: club delete on RegisterModel --- .../java/fr/titionfire/ffsaf/data/model/RegisterModel.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 953a637..02c0f90 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -8,6 +8,8 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Getter @Setter @@ -38,6 +40,7 @@ public class RegisterModel { @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "club") + @OnDelete(action = OnDeleteAction.SET_NULL) ClubModel club = null; @Column(nullable = false, columnDefinition = "boolean default false") -- 2.49.0 From beb40db1b18df579e9bc5f2f660a46ba48382626 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 12 Nov 2025 14:56:28 +0100 Subject: [PATCH 17/59] feat: merge rna and siret fields --- .../data/model/AffiliationRequestModel.java | 3 +- .../ffsaf/data/model/ClubModel.java | 7 +- .../ffsaf/domain/entity/ClubEntity.java | 6 +- .../domain/service/AffiliationService.java | 36 +++--- .../ffsaf/domain/service/ClubService.java | 17 ++- .../rest/AffiliationRequestEndpoints.java | 4 +- .../titionfire/ffsaf/rest/AssoEndpoints.java | 16 +-- .../ffsaf/rest/client/SirenService.java | 19 ---- .../ffsaf/rest/client/StateIdService.java | 17 +++ .../titionfire/ffsaf/rest/data/AssoData.java | 38 +++++++ .../ffsaf/rest/data/RenewAffData.java | 4 +- .../ffsaf/rest/data/SimpleAffiliation.java | 6 +- .../ffsaf/rest/data/SimpleClub.java | 9 +- .../ffsaf/rest/data/SimpleClubList.java | 6 +- .../ffsaf/rest/data/SimpleReqAffiliation.java | 9 +- .../rest/data/SimpleReqAffiliationResume.java | 8 +- .../ffsaf/rest/data/UniteLegaleRoot.java | 107 ------------------ .../rest/from/AffiliationRequestForm.java | 13 +-- .../rest/from/AffiliationRequestSaveForm.java | 13 +-- .../ffsaf/rest/from/FullClubForm.java | 10 +- src/main/resources/application.properties | 3 +- src/main/webapp/src/pages/DemandeAff.jsx | 71 +++++------- .../admin/affiliation/AffiliationReqList.jsx | 4 +- .../admin/affiliation/AffiliationReqPage.jsx | 6 +- .../webapp/src/pages/admin/club/ClubList.jsx | 4 +- .../webapp/src/pages/admin/club/ClubPage.jsx | 3 +- .../src/pages/admin/club/NewClubPage.jsx | 3 +- .../webapp/src/pages/club/club/MyClubPage.jsx | 3 +- src/main/webapp/vite.config.js | 2 +- 29 files changed, 163 insertions(+), 284 deletions(-) delete mode 100644 src/main/java/fr/titionfire/ffsaf/rest/client/SirenService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java create mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java delete mode 100644 src/main/java/fr/titionfire/ffsaf/rest/data/UniteLegaleRoot.java diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java index 622d883..24a1011 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/AffiliationRequestModel.java @@ -21,8 +21,7 @@ public class AffiliationRequestModel { Long id; String name; - long siret; - String RNA; + String state_id; String address; String contact; diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java index 8a5dd38..64c6036 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/ClubModel.java @@ -55,11 +55,8 @@ public class ClubModel implements LoggableModel { @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris") String address; - @Schema(description = "RNA du club", example = "W123456789") - String RNA; - - @Schema(description = "Numéro SIRET du club", example = "12345678901234") - Long SIRET; + @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234") + String StateId; @Schema(description = "Numéro d'affiliation du club", example = "12345") Long no_affiliation; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java b/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java index cb840e9..18e65b1 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/entity/ClubEntity.java @@ -22,8 +22,7 @@ public class ClubEntity { private String training_location; private String training_day_time; private String contact_intern; - private String RNA; - private Long SIRET; + private String StateId; private Long no_affiliation; private boolean international; @@ -41,8 +40,7 @@ public class ClubEntity { .training_location(model.getTraining_location()) .training_day_time(model.getTraining_day_time()) .contact_intern(model.getContact_intern()) - .RNA(model.getRNA()) - .SIRET(model.getSIRET()) + .StateId(model.getStateId()) .no_affiliation(model.getNo_affiliation()) .international(model.isInternational()) .build(); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 3bafd63..3096633 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -80,14 +80,14 @@ public class AffiliationService { throw new DBadRequestException("Saison non valid"); } })) - .chain(() -> repositoryRequest.count("siret = ?1 and saison = ?2", affModel.getSiret(), + .chain(() -> repositoryRequest.count("state_id = ?1 and saison = ?2", affModel.getState_id(), affModel.getSaison())) .onItem().invoke(Unchecked.consumer(count -> { if (count != 0 && unique) { throw new DBadRequestException("Demande d'affiliation déjà existante"); } })) - .chain(() -> clubRepository.find("SIRET = ?1", affModel.getSiret()).firstResult().chain(club -> + .chain(() -> clubRepository.find("StateId = ?1", affModel.getState_id()).firstResult().chain(club -> repository.count("club = ?1 and saison = ?2", club, affModel.getSaison()))) .onItem().invoke(Unchecked.consumer(count -> { if (count != 0) { @@ -124,7 +124,6 @@ public class AffiliationService { .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .chain(origine -> { origine.setName(model.getName()); - origine.setRNA(model.getRNA()); origine.setAddress(model.getAddress()); origine.setContact(model.getContact()); origine.setM1_lname(model.getM1_lname()); @@ -181,8 +180,7 @@ public class AffiliationService { .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .map(model -> { model.setName(form.getName()); - model.setSiret(form.getSiret()); - model.setRNA(form.getRna()); + model.setState_id(form.getState_id()); model.setAddress(form.getAddress()); model.setContact(form.getContact()); @@ -289,10 +287,12 @@ public class AffiliationService { return repositoryRequest.findById(form.getId()) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) .chain(req -> - clubRepository.find("SIRET = ?1", form.getSiret()).firstResult() + clubRepository.find("StateId = ?1", form.getState_id()).firstResult() .chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model)) - .call(club -> setMembre(form.new Member(1), club, req.getSaison()) - .call(__ -> setMembre(form.new Member(2), club, req.getSaison()) + .call(club -> setMembre(form.new Member(1), club, req.getSaison()).onFailure() + .recoverWithNull() + .call(__ -> setMembre(form.new Member(2), club, req.getSaison()).onFailure() + .recoverWithNull() .call(___ -> setMembre(form.new Member(3), club, req.getSaison())))) .onItem() .invoke(model -> Uni.createFrom() @@ -317,8 +317,7 @@ public class AffiliationService { ClubModel club = new ClubModel(); club.setName(form.getName()); club.setCountry("FR"); - club.setSIRET(form.getSiret()); - club.setRNA(form.getRna()); + club.setStateId(form.getState_id()); club.setAddress(form.getAddress()); club.setContact_intern(form.getContact()); club.setAffiliations(new ArrayList<>()); @@ -355,8 +354,7 @@ public class AffiliationService { .chain(() -> { club.setName(form.getName()); club.setCountry("FR"); - club.setSIRET(form.getSiret()); - club.setRNA(form.getRna()); + club.setStateId(form.getState_id()); club.setAddress(form.getAddress()); club.setContact_intern(form.getContact()); return Panache.withTransaction(() -> clubRepository.persist(club) @@ -369,7 +367,7 @@ public class AffiliationService { public Uni getRequest(long id) { return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel) .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) - .call(out -> clubRepository.find("SIRET = ?1", out.getSiret()).firstResult().invoke(c -> { + .call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> { if (c != null) { out.setClub(c.getId()); out.setClub_name(c.getName()); @@ -382,7 +380,7 @@ public class AffiliationService { public Uni> getCurrentSaisonAffiliation() { return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison()) .map(models -> models.stream() - .map(model -> new SimpleAffiliation(model.getId() * -1, model.getSiret(), model.getSaison(), + .map(model -> new SimpleAffiliation(model.getId() * -1, model.getState_id(), model.getSaison(), false)).toList()) .chain(aff -> repository.list("saison = ?1", Utils.getSaison()) .map(models -> models.stream().map(SimpleAffiliation::fromModel).toList()) @@ -394,9 +392,9 @@ public class AffiliationService { return clubRepository.findById(id) .onItem().ifNull().failWith(new DNotFoundException("Club non trouvé")) .call(model -> Mutiny.fetch(model.getAffiliations())) - .chain(model -> repositoryRequest.list("siret = ?1", model.getSIRET()) + .chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId()) .map(reqs -> reqs.stream().map(req -> - new SimpleAffiliation(req.getId() * -1, model.getId(), req.getSaison(), false))) + new SimpleAffiliation(req.getId() * -1, model.getStateId(), req.getSaison(), false))) .map(aff2 -> Stream.concat(aff2, model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList()) ); @@ -426,9 +424,9 @@ public class AffiliationService { return Panache.withTransaction(() -> repository.deleteById(id)); } - public Uni deleteReqAffiliation(long id, String reason) { + public Uni deleteReqAffiliation(long id, String reason, boolean federationAdmin) { return repositoryRequest.findById(id) - .call(aff -> reactiveMailer.send( + .call(aff -> federationAdmin ? reactiveMailer.send( Mail.withText(aff.getM1_email(), "FFSAF - Votre demande d'affiliation a été rejetée.", String.format( @@ -445,7 +443,7 @@ public class AffiliationService { """, aff.getName(), reason) ).setFrom("FFSAF ").setReplyTo("contact@ffsaf.fr") .addTo(aff.getM2_email(), aff.getM3_email()) - )) + ) : Uni.createFrom().nullItem()) .chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff))) .call(__ -> Utils.deleteMedia(id, media, "aff_request/logo")) .call(__ -> Utils.deleteMedia(id, media, "aff_request/status")); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index 40a6155..f9d8a76 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -211,11 +211,9 @@ public class ClubService { m.setTraining_day_time(input.getTraining_day_time()); ls.logChange("Contact interne", m.getContact_intern(), input.getContact_intern(), m); m.setContact_intern(input.getContact_intern()); - ls.logChange("N° RNA", m.getRNA(), input.getRna(), m); - m.setRNA(input.getRna()); - if (input.getSiret() != null && !input.getSiret().isBlank()) { - ls.logChange("N° SIRET", m.getSIRET(), input.getSiret(), m); - m.setSIRET(Long.parseLong(input.getSiret())); + if (input.getState_id() != null && !input.getState_id().isBlank()) { + ls.logChange("N° SIRET", m.getClubId(), input.getState_id(), m); + m.setStateId(input.getState_id()); } ls.logChange("Adresse administrative", m.getAddress(), input.getAddress(), m); m.setAddress(input.getAddress()); @@ -251,9 +249,8 @@ public class ClubService { clubModel.setTraining_location(input.getTraining_location()); clubModel.setTraining_day_time(input.getTraining_day_time()); clubModel.setContact_intern(input.getContact_intern()); - clubModel.setRNA(input.getRna()); - if (input.getSiret() != null && !input.getSiret().isBlank()) - clubModel.setSIRET(Long.parseLong(input.getSiret())); + if (input.getState_id() != null && !input.getState_id().isBlank()) + clubModel.setStateId(input.getState_id()); clubModel.setAddress(input.getAddress()); try { @@ -300,9 +297,9 @@ public class ClubService { .call(clubModel -> Mutiny.fetch(clubModel.getAffiliations())) .invoke(clubModel -> { data.setName(clubModel.getName()); - data.setSiret(clubModel.getSIRET()); - data.setRna(clubModel.getRNA()); + data.setState_id(clubModel.getStateId()); data.setAddress(clubModel.getAddress()); + data.setContact(clubModel.getContact_intern()); data.setSaison( clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison)) .map(AffiliationModel::getSaison).map(i -> Math.min(i + 1, Utils.getSaison() + 1)) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java index fcb2d6b..16ddb09 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AffiliationRequestEndpoints.java @@ -92,7 +92,7 @@ public class AffiliationRequestEndpoints { @DELETE @Path("/{id}") - @RolesAllowed({"federation_admin"}) + @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " + "d'affiliation pour l'identifiant spécifié.") @@ -107,7 +107,7 @@ public class AffiliationRequestEndpoints { if (o.getClub() == null && !securityCtx.roleHas("federation_admin")) throw new DForbiddenException(); })).invoke(o -> checkPerm.accept(o.getClub())) - .chain(o -> service.deleteReqAffiliation(id, reason)); + .chain(o -> service.deleteReqAffiliation(id, reason, securityCtx.roleHas("federation_admin"))); } @PUT diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java index a7904c5..c81b955 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java @@ -1,7 +1,7 @@ package fr.titionfire.ffsaf.rest; -import fr.titionfire.ffsaf.rest.client.SirenService; -import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot; +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.ws.rs.*; @@ -13,17 +13,17 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; public class AssoEndpoints { @RestClient - SirenService sirenService; + StateIdService stateIdService; @GET - @Path("siren/{siren}") + @Path("state_id/{stateId}") @Produces(MediaType.APPLICATION_JSON) @Operation(hidden = true) - public Uni getInfoSiren(@PathParam("siren") String siren) { - return sirenService.get_unite(siren).onFailure().transform(throwable -> { + public Uni getAssoInfo(@PathParam("stateId") String stateId) { + return stateIdService.get_status(stateId).onFailure().transform(throwable -> { if (throwable instanceof WebApplicationException exception) { - if (exception.getResponse().getStatus() == 400) - return new DNotFoundException("Siret introuvable"); + if (exception.getResponse().getStatus() == 404) + return new DNotFoundException("Asso introuvable"); } return throwable; }); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/client/SirenService.java b/src/main/java/fr/titionfire/ffsaf/rest/client/SirenService.java deleted file mode 100644 index 413ddc7..0000000 --- a/src/main/java/fr/titionfire/ffsaf/rest/client/SirenService.java +++ /dev/null @@ -1,19 +0,0 @@ -package fr.titionfire.ffsaf.rest.client; - -import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot; -import io.smallrye.mutiny.Uni; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; - -@Path("/") -@RegisterRestClient -@ClientHeaderParam(name = "X-Client-Secret", value = "${siren-api.key}") -public interface SirenService { - - @GET - @Path("/v3/unites_legales/{SIREN}") - Uni get_unite(@PathParam("SIREN") String siren); -} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java b/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java new file mode 100644 index 0000000..9d03809 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java @@ -0,0 +1,17 @@ +package fr.titionfire.ffsaf.rest.client; + +import fr.titionfire.ffsaf.rest.data.AssoData; +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@Path("/") +@RegisterRestClient +public interface StateIdService { + + @GET + @Path("/structure/{id}") + Uni get_status(@PathParam("id") String id); +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java new file mode 100644 index 0000000..a7e1f39 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java @@ -0,0 +1,38 @@ +package fr.titionfire.ffsaf.rest.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; + +@Data +@RegisterForReflection +public class AssoData { + String id_siren; + String id_rna; + Identite identite; + Coordonnee coordonnees; + + @Data + @RegisterForReflection + public static class Identite { + String nom; + } + + @Data + @RegisterForReflection + public static class Coordonnee { + Address adresse_siege; + } + + @Data + @RegisterForReflection + public static class Address { + String cplt_1; + String cplt_2; + String cplt_3; + String num_voie; + String type_voie; + String voie; + String cp; + String commune; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java index 02ce7cc..4744054 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/RenewAffData.java @@ -17,9 +17,9 @@ import java.util.List; @RegisterForReflection public class RenewAffData { String name; - Long siret; - String rna; + String state_id; String address; + String contact; int saison; List members; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java index 2631783..edc4299 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleAffiliation.java @@ -14,8 +14,8 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; public class SimpleAffiliation { @Schema(description = "L'identifiant de l'affiliation.", example = "1") private Long id; - @Schema(description = "L'identifiant du club associé à l'affiliation.", example = "123") - private Long club; + @Schema(description = "L'identifiant du club associé à l'affiliation si id > 0 sinon n° SIRET ou RNA du club.", example = "123") + private String club; @Schema(description = "La saison de l'affiliation.", example = "2022") private int saison; @Schema(description = "Indique si l'affiliation est validée ou non.", example = "true") @@ -27,7 +27,7 @@ public class SimpleAffiliation { return new SimpleAffiliationBuilder() .id(model.getId()) - .club(model.getClub().getId()) + .club(String.valueOf(model.getClub().getId())) .saison(model.getSaison()) .validate(true) .build(); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java index 6229aa6..cbb904b 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClub.java @@ -36,10 +36,8 @@ public class SimpleClub { private String contact_intern; @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris") private String address; - @Schema(description = "RNA du club", example = "W123456789") - private String RNA; - @Schema(description = "Numéro SIRET du club", example = "12345678901234") - private Long SIRET; + @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234") + private String state_id; @Schema(description = "Numéro d'affiliation du club", example = "12345") private Long no_affiliation; @Schema(description = "Club international", example = "false") @@ -60,8 +58,7 @@ public class SimpleClub { .training_location(model.getTraining_location()) .training_day_time(model.getTraining_day_time()) .contact_intern(model.getContact_intern()) - .RNA(model.getRNA()) - .SIRET(model.getSIRET()) + .state_id(model.getStateId()) .no_affiliation(model.getNo_affiliation()) .international(model.isInternational()) .address(model.getAddress()) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java index b42edb6..d523d7d 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleClubList.java @@ -20,8 +20,8 @@ public class SimpleClubList { String name; @Schema(description = "Pays du club", example = "FR") String country; - @Schema(description = "Numéro SIRET du club", example = "12345678901234") - Long siret; + @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234") + String state_id; @Schema(description = "Numéro d'affiliation du club", example = "12345") Long no_affiliation; @@ -29,7 +29,7 @@ public class SimpleClubList { if (model == null) return null; - return new SimpleClubList(model.getId(), model.getName(), model.getCountry(), model.getSIRET(), + return new SimpleClubList(model.getId(), model.getName(), model.getCountry(), model.getStateId(), model.getNo_affiliation()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java index ea0ef71..30ab3ac 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliation.java @@ -25,10 +25,8 @@ public class SimpleReqAffiliation { Long club_no_aff; @Schema(description = "Nom du club demander", example = "Association sportive") String name; - @Schema(description = "Numéro SIRET de l'association", example = "12345678901234") - long siret; - @Schema(description = "Numéro RNA de l'association", example = "W123456789") - String RNA; + @Schema(description = "Numéro SIRET ou RNA de l'association", example = "12345678901234") + String stateId; @Schema(description = "Adresse de l'association", example = "1 rue de l'exemple, 75000 Paris") String address; @Schema(description = "Email de contact de l'association", example = "test@test.fr") @@ -45,8 +43,7 @@ public class SimpleReqAffiliation { return new SimpleReqAffiliation.SimpleReqAffiliationBuilder() .id(model.getId()) .name(model.getName()) - .siret(model.getSiret()) - .RNA(model.getRNA()) + .stateId(model.getState_id()) .address(model.getAddress()) .saison(model.getSaison()) .contact(model.getContact()) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java index f99ace0..1f73405 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/SimpleReqAffiliationResume.java @@ -16,8 +16,8 @@ public class SimpleReqAffiliationResume { Long id; @Schema(description = "Le nom de l'association.", example = "Association sportive") String name; - @Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234") - long siret; + @Schema(description = "Le numéro SIRET ou RNA de l'association.", example = "12345678901234") + String stateId; @Schema(description = "La saison de l'affiliation.", example = "2025") int saison; @@ -25,10 +25,10 @@ public class SimpleReqAffiliationResume { if (model == null) return null; - return new SimpleReqAffiliationResume.SimpleReqAffiliationResumeBuilder() + return new SimpleReqAffiliationResumeBuilder() .id(model.getId()) .name(model.getName()) - .siret(model.getSiret()) + .stateId(model.getState_id()) .saison(model.getSaison()) .build(); } diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/UniteLegaleRoot.java b/src/main/java/fr/titionfire/ffsaf/rest/data/UniteLegaleRoot.java deleted file mode 100644 index 2b5c34a..0000000 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/UniteLegaleRoot.java +++ /dev/null @@ -1,107 +0,0 @@ -package fr.titionfire.ffsaf.rest.data; - -import io.quarkus.runtime.annotations.RegisterForReflection; -import lombok.Data; - -import java.util.ArrayList; -import java.util.Date; - -@Data -@RegisterForReflection -public class UniteLegaleRoot { - public UniteLegale unite_legale; - - @Data - @RegisterForReflection - public static class UniteLegale { - public String activite_principale; - public Object annee_categorie_entreprise; - public Object annee_effectifs; - public Object caractere_employeur; - public Object categorie_entreprise; - public String categorie_juridique; - public String date_creation; - public String date_debut; - public Date date_dernier_traitement; - public String denomination; - public Object denomination_usuelle_1; - public Object denomination_usuelle_2; - public Object denomination_usuelle_3; - public String economie_sociale_solidaire; - public Etablissement etablissement_siege; - public ArrayList etablissements; - public String etat_administratif; - public String identifiant_association; - public String nic_siege; - public Object nom; - public Object nom_usage; - public int nombre_periodes; - public String nomenclature_activite_principale; - public Object prenom_1; - public Object prenom_2; - public Object prenom_3; - public Object prenom_4; - public Object prenom_usuel; - public Object pseudonyme; - public Object sexe; - public Object sigle; - public String siren; - public String societe_mission; - public String statut_diffusion; - public Object tranche_effectifs; - public Object unite_purgee; - } - - @Data - @RegisterForReflection - public static class Etablissement { - private String activite_principale; - private Object activite_principale_registre_metiers; - private Object annee_effectifs; - private String caractere_employeur; - private Object code_cedex; - private Object code_cedex_2; - private String code_commune; - private Object code_commune_2; - private Object code_pays_etranger; - private Object code_pays_etranger_2; - private String code_postal; - private Object code_postal_2; - private Object complement_adresse; - private Object complement_adresse2; - private String date_creation; - private String date_debut; - private Date date_dernier_traitement; - private Object denomination_usuelle; - private Object distribution_speciale; - private Object distribution_speciale_2; - private Object enseigne_1; - private Object enseigne_2; - private Object enseigne_3; - private boolean etablissement_siege; - private String etat_administratif; - private Object indice_repetition; - private Object indice_repetition_2; - private Object libelle_cedex; - private Object libelle_cedex_2; - private String libelle_commune; - private Object libelle_commune_2; - private Object libelle_commune_etranger; - private Object libelle_commune_etranger_2; - private Object libelle_pays_etranger; - private Object libelle_pays_etranger_2; - private String libelle_voie; - private Object libelle_voie_2; - private String nic; - private int nombre_periodes; - private String nomenclature_activite_principale; - private String numero_voie; - private Object numero_voie_2; - private String siren; - private String siret; - private String statut_diffusion; - private Object tranche_effectifs; - private String type_voie; - private Object type_voie_2; - } -} \ No newline at end of file diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java index 9dfb985..ae657de 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestForm.java @@ -21,13 +21,9 @@ public class AffiliationRequestForm { @FormParam("name") private String name = null; - @Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234", required = true) - @FormParam("siret") - private Long siret = null; - - @Schema(description = "Le numéro RNA de l'association. (peut être null)", example = "W123456789") - @FormParam("rna") - private String rna = null; + @Schema(description = "Le numéro SIRET/RNA de l'association.", example = "12345678901234", required = true) + @FormParam("state_id") + private String state_id = null; @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) @FormParam("adresse") @@ -114,8 +110,7 @@ public class AffiliationRequestForm { public AffiliationRequestModel toModel() { AffiliationRequestModel model = new AffiliationRequestModel(); model.setName(this.getName()); - model.setSiret(this.getSiret()); - model.setRNA(this.getRna()); + model.setState_id(this.getState_id()); model.setAddress(this.getAdresse()); model.setSaison(this.getSaison()); model.setContact(this.getContact()); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java index 36f62e9..a1f66aa 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/AffiliationRequestSaveForm.java @@ -17,13 +17,9 @@ public class AffiliationRequestSaveForm { @FormParam("name") private String name = null; - @Schema(description = "Le numéro SIRET de l'association.", example = "12345678901234", required = true) - @FormParam("siret") - private Long siret = null; - - @Schema(description = "Le numéro RNA de l'association. (peut être null)", example = "W123456789") - @FormParam("rna") - private String rna = null; + @Schema(description = "Le numéro SIRET ou RNA de l'association.", example = "12345678901234", required = true) + @FormParam("state_id") + private String state_id = null; @Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true) @FormParam("address") @@ -175,8 +171,7 @@ public class AffiliationRequestSaveForm { return "AffiliationRequestSaveForm{" + "id=" + id + ", name='" + name + '\'' + - ", siret=" + siret + - ", rna='" + rna + '\'' + + ", state_id=" + state_id + ", address='" + address + '\'' + ", contact='" + contact + '\'' + ", status_len=" + status.length + diff --git a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java index 26f81f1..6a23b52 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/from/FullClubForm.java @@ -43,13 +43,9 @@ public class FullClubForm { @Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris", required = true) private String address = null; - @FormParam("rna") - @Schema(description = "RNA du club", example = "W123456789") - private String rna = null; - - @FormParam("siret") - @Schema(description = "Numéro SIRET du club", example = "12345678901234", required = true) - private String siret = null; + @FormParam("state_id") + @Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234", required = true) + private String state_id = null; @FormParam("international") @Schema(description = "Club international", example = "false", required = true) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0e1ae68..387faeb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,8 +41,7 @@ database.pass= notif.affRequest.mail= -siren-api.key=siren-ap -quarkus.rest-client."fr.titionfire.ffsaf.rest.client.SirenService".url=https://data.siren-api.fr/ +quarkus.rest-client."fr.titionfire.ffsaf.rest.client.StateIdService".url=https://siva-int.menjes.ate.info/apim/api-asso/api/ #Login quarkus.oidc.token-state-manager.split-tokens=true diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index d0a2953..e2e46ee 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -16,28 +16,26 @@ function formatAdresse(data) { }).join(" "); } -function reconstruireAdresse(infos) { - console.log(infos); +function reconstruireAdresse2(infos) { let adresseReconstruite = ""; - if (infos.numero_voie === null) { - if (infos.complement_adresse) { - adresseReconstruite += formatAdresse(infos.complement_adresse) + ', '; - } - } else { - adresseReconstruite += infos.numero_voie + ' '; + if (infos?.cplt_1) { + adresseReconstruite += formatAdresse(infos.cplt_1) + ', '; + } + if (infos?.cplt_2) { + adresseReconstruite += formatAdresse(infos.cplt_2) + ', '; + } + if (infos?.cplt_3) { + adresseReconstruite += formatAdresse(infos.cplt_3) + ', '; + } + + if (infos?.num_voie) { + adresseReconstruite += infos.num_voie + ' '; } adresseReconstruite += formatAdresse(infos.type_voie) + ' '; - adresseReconstruite += formatAdresse(infos.libelle_voie) + ', '; - adresseReconstruite += infos.code_postal + ' ' + infos.libelle_commune + ', '; - - if (infos.complement_adresse && infos.numero_voie !== null) { - adresseReconstruite += formatAdresse(infos.complement_adresse) + ', '; - } - if (infos.code_cedex && infos.libelle_cedex) { - adresseReconstruite += 'Cedex ' + infos.code_cedex + ' - ' + infos.libelle_cedex; - } + adresseReconstruite += formatAdresse(infos.voie) + ', '; + adresseReconstruite += infos.cp + ' ' + infos.commune + ', '; if (adresseReconstruite.endsWith(', ')) { adresseReconstruite = adresseReconstruite.slice(0, -2); @@ -85,8 +83,7 @@ export function DemandeAff() { event.preventDefault() const formData = new FormData(event.target) formData.append("m1_role", event.target.m1_role?.value) - formData.append("rna", event.target.rna?.value) - formData.append("siret", event.target.siret?.value) + formData.append("state_id", event.target.state_id?.value) let error = false; for (let i = 1; i <= 3; i++) { @@ -219,20 +216,19 @@ export function DemandeAff() { function AssoInfo({initData, needFile}) { const [denomination, setDenomination] = useState("") - const [siret, setSiret] = useState(initData.siret ? String(initData.siret) : "") - const [rna, setRna] = useState(initData.rna ? initData.rna : "") - const [rnaEnable, setRnaEnable] = useState(false) + 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 fetchSiret = () => { - if (siret.length < 14) { - toast.error("Le SIRET doit contenir 14 chiffres") + const fetchStateId = () => { + const regex = /^(?:\d{14}|W?\d{9})$/; + if (!regex.test(stateId)) { + toast.error("Le format du SIRET/RNA est invalide"); return; } toast.promise( - apiAxios.get(`asso/siren/${siret.substring(0, siret.length - 5)}`), + apiAxios.get(`asso/state_id/${stateId}`), { pending: "Recherche de l'association en cours", success: "Association trouvée avec succès 🎉", @@ -243,12 +239,10 @@ function AssoInfo({initData, needFile}) { } } ).then(data => { - const data2 = data.data.unite_legale - setDenomination(data2.denomination) - setRnaEnable(data2.identifiant_association === null) - setRna(data2.identifiant_association ? data2.identifiant_association : "") + const data2 = data.data + setDenomination(data2.identite.nom) if (!initData.saison || adresse === "") - setAdresse(reconstruireAdresse(data2.etablissement_siege)) + setAdresse(reconstruireAdresse2(data2.coordonnees.adresse_siege)) }) } return <> @@ -262,11 +256,11 @@ function AssoInfo({initData, needFile}) {
- N° SIRET* - setSiret(e.target.value)}/> + N° SIRET ou RNA* + setStateId(e.target.value)}/>
@@ -277,13 +271,6 @@ function AssoInfo({initData, needFile}) { aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
-
- RNA - setRna(e.target.value)}/> -
-
Adresse administrative* diff --git a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqList.jsx b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqList.jsx index 54e9d65..a60a762 100644 --- a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqList.jsx +++ b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqList.jsx @@ -68,7 +68,7 @@ function MakeRow({request, navigate}) {
{request.name}
- {request.saison}-{request.saison + 1}
{request.siret}
+ {request.saison}-{request.saison + 1}
{request.state_id}
} @@ -104,4 +104,4 @@ function Def() {
  • -} \ No newline at end of file +} diff --git a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx index cb292bb..6fc3830 100644 --- a/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx +++ b/src/main/webapp/src/pages/admin/affiliation/AffiliationReqPage.jsx @@ -66,8 +66,7 @@ function Content({data, refresh}) { formData.append('id', data.id); formData.append('name', event.target.name.value); - formData.append('siret', event.target.siret.value); - formData.append('rna', event.target.rna.value); + formData.append('state_id', event.target.state_id.value); formData.append('address', event.target.address.value); formData.append('contact', event.target.contact.value); @@ -177,8 +176,7 @@ function Content({data, refresh}) { {data.club &&
    Ancien nom: {data.club_name}
    }
    - - + diff --git a/src/main/webapp/src/pages/admin/club/ClubList.jsx b/src/main/webapp/src/pages/admin/club/ClubList.jsx index 85941c8..5be10fb 100644 --- a/src/main/webapp/src/pages/admin/club/ClubList.jsx +++ b/src/main/webapp/src/pages/admin/club/ClubList.jsx @@ -40,7 +40,7 @@ export function ClubList() { country: e.country, siret: e.siret, no_affiliation: e.no_affiliation, - affiliation: showAffiliationState ? affiliationData.find(aff => (aff.id >= 0) ? aff.club === e.id : aff.club === e.siret) : null + affiliation: showAffiliationState ? affiliationData.find(aff => (aff.id >= 0) ? Number(aff.club) === e.id : aff.club === e.state_id) : null }) } setClubData(data2); @@ -197,4 +197,4 @@ function Def() {
  • -} \ No newline at end of file +} diff --git a/src/main/webapp/src/pages/admin/club/ClubPage.jsx b/src/main/webapp/src/pages/admin/club/ClubPage.jsx index 5a6768a..a6a3e76 100644 --- a/src/main/webapp/src/pages/admin/club/ClubPage.jsx +++ b/src/main/webapp/src/pages/admin/club/ClubPage.jsx @@ -130,8 +130,7 @@ function InformationForm({data}) {
    {!switchOn && <> - - +
    {!switchOn && <> - - + diff --git a/src/main/webapp/src/pages/club/club/MyClubPage.jsx b/src/main/webapp/src/pages/club/club/MyClubPage.jsx index d8e0fb6..a4f9ae4 100644 --- a/src/main/webapp/src/pages/club/club/MyClubPage.jsx +++ b/src/main/webapp/src/pages/club/club/MyClubPage.jsx @@ -75,8 +75,7 @@ function InformationForm({data}) { {!data.international && <> - - + }
    diff --git a/src/main/webapp/vite.config.js b/src/main/webapp/vite.config.js index c532ed0..6b5b768 100644 --- a/src/main/webapp/vite.config.js +++ b/src/main/webapp/vite.config.js @@ -24,4 +24,4 @@ export default ({mode}) => { }, }, }); -}; \ No newline at end of file +}; -- 2.49.0 From 645949a2f6db032ddd2e41c03b60db87ec5fadc8 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 12 Nov 2025 16:15:40 +0100 Subject: [PATCH 18/59] feat: add cache to getAssoInfo --- src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java | 2 ++ .../java/fr/titionfire/ffsaf/rest/client/StateIdService.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java index c81b955..321a9c1 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java @@ -23,6 +23,8 @@ public class AssoEndpoints { return stateIdService.get_status(stateId).onFailure().transform(throwable -> { if (throwable instanceof WebApplicationException exception) { if (exception.getResponse().getStatus() == 404) + return new DNotFoundException("Service momentanément indisponible"); + if (exception.getResponse().getStatus() == 400) return new DNotFoundException("Asso introuvable"); } return throwable; diff --git a/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java b/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java index 9d03809..e13c190 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/client/StateIdService.java @@ -1,6 +1,7 @@ package fr.titionfire.ffsaf.rest.client; import fr.titionfire.ffsaf.rest.data.AssoData; +import io.quarkus.cache.CacheResult; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -13,5 +14,6 @@ public interface StateIdService { @GET @Path("/structure/{id}") + @CacheResult(cacheName = "AssoData_status") Uni get_status(@PathParam("id") String id); } -- 2.49.0 From a7ba1d16a4e0fb9217f566406e0d63adee1362c1 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 12 Nov 2025 16:38:35 +0100 Subject: [PATCH 19/59] feat: remove kc new account mail --- .../ffsaf/domain/service/KeycloakService.java | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java index 5748b33..1c74ba6 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/KeycloakService.java @@ -231,9 +231,6 @@ public class KeycloakService { user.setEmail(membreModel.getEmail()); user.setEnabled(true); - user.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name(), - RequiredAction.UPDATE_PASSWORD.name())); - try (Response response = keycloak.realm(realm).users().create(user)) { if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo() .equals(Response.Status.CONFLICT)) @@ -245,13 +242,6 @@ public class KeycloakService { return getUser(login).orElseThrow( () -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin))); }) - .call(user -> enabled_email ? - vertx.getOrCreateContext().executeBlocking(() -> { - keycloak.realm(realm).users().get(user.getId()) - .executeActionsEmail(List.of(RequiredAction.VERIFY_EMAIL.name(), - RequiredAction.UPDATE_PASSWORD.name())); - return null; - }) : Uni.createFrom().nullItem()) .invoke(user -> membreModel.setUserId(user.getId())) .call(user -> updateRole(user.getId(), List.of("safca_user"), List.of())) .call(user -> enabled_email ? reactiveMailer.send( @@ -261,14 +251,14 @@ public class KeycloakService { """ Bonjour, - Suite à votre première inscription %sà la Fédération Française de Soft Armored Fighting (FFSAF), votre compte pour accéder à l'intranet a été créé. - Ce compte vous permettra de consulter vos informations, de vous inscrire aux compétitions et de consulter vos résultats. - - Vous allez recevoir dans les prochaines minutes un email vous demandant de vérifier votre email et de définir un mot de passe. + Suite à votre première inscription %sà la Fédération Française de Soft Armored Fighting (FFSAF), votre compte intranet a été créé. + Ce compte vous permettra de consulter vos informations et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats. L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr Votre nom d'utilisateur est : %s + Pour définir votre mot de passe, rendez-vous sur l'intranet > "Connexion" > "Mot de passe oublié ?" + Si vous n'avez pas demandé cette inscription, veuillez contacter le support à l'adresse support@ffsaf.fr. (Pas de panique, nous ne vous enverrons pas de message autre que ce concernant votre compte) -- 2.49.0 From 81c115c6557d2481b49d984599446cd60a1eaa65 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Nov 2025 13:43:51 +0100 Subject: [PATCH 20/59] fix: empty mail on import --- .../fr/titionfire/ffsaf/domain/service/MembreService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 51f2ac9..a4bab12 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -236,17 +236,17 @@ public class MembreService { return mm; }); - if (model.getEmail() != null) { + if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { LOGGER.info("Similar membres found: " + model); - throw new DBadRequestException("Email déja utiliser"); + throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); } if (StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { LOGGER.info("Similar membres found: " + model); - throw new DBadRequestException("Email déja utiliser"); + throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser"); } } -- 2.49.0 From 3d3d63e58c3c2ba4acda39350d3504b114a110c5 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Nov 2025 14:14:22 +0100 Subject: [PATCH 21/59] fix: add more log to import --- .../fr/titionfire/ffsaf/domain/service/MembreService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index a4bab12..58ae725 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -235,6 +235,11 @@ public class MembreService { mm.setCountry("FR"); return mm; }); + if (model.getId() != null){ + LOGGER.debugf("updating -> %s", dataIn.toString()); + }else{ + LOGGER.debugf("creating -> %s", dataIn.toString()); + } if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { -- 2.49.0 From 87b3bc12e0f08594086dfcb961dcadc6af8353b3 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Nov 2025 14:31:37 +0100 Subject: [PATCH 22/59] fix: null licence on import --- .../ffsaf/domain/service/MembreService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 58ae725..6931e26 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -224,10 +224,10 @@ public class MembreService { Uni uniResult = Uni.createFrom().voidItem(); for (SimpleMembreInOutData dataIn : data2) { MembreModel model = membres.stream() - .filter(m -> Objects.equals(m.getLicence(), dataIn.getLicence()) || m.getLname() - .equals(dataIn.getNom()) && m.getFname().equals(dataIn.getPrenom()) || - (dataIn.getEmail() != null && !dataIn.getEmail().isBlank() && Objects.equals( - m.getFname(), dataIn.getEmail()))).findFirst() + .filter(m -> (dataIn.getLicence() != null && Objects.equals(m.getLicence(), + dataIn.getLicence())) || m.getLname().equals(dataIn.getNom()) && m.getFname() + .equals(dataIn.getPrenom()) || (dataIn.getEmail() != null && !dataIn.getEmail() + .isBlank() && Objects.equals(m.getFname(), dataIn.getEmail()))).findFirst() .orElseGet(() -> { MembreModel mm = new MembreModel(); mm.setClub(clubModel.get()); @@ -235,9 +235,9 @@ public class MembreService { mm.setCountry("FR"); return mm; }); - if (model.getId() != null){ + if (model.getId() != null) { LOGGER.debugf("updating -> %s", dataIn.toString()); - }else{ + } else { LOGGER.debugf("creating -> %s", dataIn.toString()); } -- 2.49.0 From c2eecf490614151fc5618e496487f198568f4d67 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Nov 2025 14:46:58 +0100 Subject: [PATCH 23/59] fix: certifDate NaN all membre export --- src/main/webapp/src/pages/MemberList.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx index a2d3fc7..112fef8 100644 --- a/src/main/webapp/src/pages/MemberList.jsx +++ b/src/main/webapp/src/pages/MemberList.jsx @@ -169,6 +169,10 @@ function FileOutput() { certifDate: e.certif ? new Date(e.certif.split("¤")[1]) : '', } + if (isNaN(tmp.certifDate) || tmp.certifDate === 'NaN') { + tmp.certifDate = '' + } + //tmp.birthdate.setMilliseconds(0); //tmp.birthdate.setSeconds(0); //tmp.birthdate.setMinutes(0); -- 2.49.0 From aebcd62aa9f285536bb851e3f38f700419268229 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Fri, 14 Nov 2025 16:17:40 +0100 Subject: [PATCH 24/59] feat: upgrade aff find to user all id (Rna, siret, siren) --- .../domain/service/AffiliationService.java | 33 ++++++++++++++----- .../titionfire/ffsaf/rest/data/AssoData.java | 1 + src/main/webapp/src/pages/DemandeAff.jsx | 2 +- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 3096633..a88ec0f 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.repository.*; +import fr.titionfire.ffsaf.rest.client.StateIdService; import fr.titionfire.ffsaf.rest.data.SimpleAffiliation; import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation; import fr.titionfire.ffsaf.rest.exception.DBadRequestException; @@ -21,6 +22,7 @@ import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; import org.hibernate.reactive.mutiny.Mutiny; import org.jboss.logging.Logger; @@ -60,6 +62,9 @@ public class AffiliationService { @Inject LoggerService ls; + @RestClient + StateIdService stateIdService; + @ConfigProperty(name = "upload_dir") String media; @@ -73,6 +78,8 @@ public class AffiliationService { public Uni pre_save(AffiliationRequestForm form, boolean unique) { AffiliationRequestModel affModel = form.toModel(); int currentSaison = Utils.getSaison(); + List out = new ArrayList<>(); + out.add(affModel.getState_id()); return Uni.createFrom().item(affModel) .invoke(Unchecked.consumer(model -> { @@ -80,14 +87,24 @@ public class AffiliationService { throw new DBadRequestException("Saison non valid"); } })) - .chain(() -> repositoryRequest.count("state_id = ?1 and saison = ?2", affModel.getState_id(), - affModel.getSaison())) - .onItem().invoke(Unchecked.consumer(count -> { - if (count != 0 && unique) { - throw new DBadRequestException("Demande d'affiliation déjà existante"); - } - })) - .chain(() -> clubRepository.find("StateId = ?1", affModel.getState_id()).firstResult().chain(club -> + .chain(() -> stateIdService.get_status(affModel.getState_id()).onItem().transform(o -> { + if (o.getId_rna() != null && !o.getId_rna().isBlank()) + out.add(o.getId_rna()); + if (o.getId_siren() != null && !o.getId_siren().isBlank()) + out.add(o.getId_siren()); + if (o.getIdentite().getId_siret_siege() != null && !o.getIdentite().getId_siret_siege().isBlank()) + out.add(o.getIdentite().getId_siret_siege()); + return out; + }).onFailure().recoverWithItem(out) + .chain(a -> repositoryRequest.count("state_id IN ?1 and saison = ?2", + out, affModel.getSaison())) + .onItem().invoke(Unchecked.consumer(count -> { + if (count != 0 && unique) { + throw new DBadRequestException("Demande d'affiliation déjà existante"); + } + })) + ) + .chain(() -> clubRepository.find("StateId IN ?1", out).firstResult().chain(club -> repository.count("club = ?1 and saison = ?2", club, affModel.getSaison()))) .onItem().invoke(Unchecked.consumer(count -> { if (count != 0) { diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java index a7e1f39..4bb71fb 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/AssoData.java @@ -15,6 +15,7 @@ public class AssoData { @RegisterForReflection public static class Identite { String nom; + String id_siret_siege; } @Data diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx index e2e46ee..8df8659 100644 --- a/src/main/webapp/src/pages/DemandeAff.jsx +++ b/src/main/webapp/src/pages/DemandeAff.jsx @@ -257,7 +257,7 @@ function AssoInfo({initData, needFile}) {
    N° SIRET ou RNA* - setStateId(e.target.value)}/> }
    + +
    +
    Trie
    +
    + +
    +
    +
    Filtre
    @@ -371,7 +389,7 @@ function MakeRow({member, showLicenceState, navigate, source}) { const rowContent = <>
    {(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "} - {(showLicenceState && member.licence != null && member.licence.pay)? : <>  } + {(showLicenceState && member.licence != null && member.licence.pay) ? : <>  }
    {member.fname} {member.lname}
    @@ -394,9 +412,92 @@ function MakeRow({member, showLicenceState, navigate, source}) { } } +function OrderBar({onOrderChange, source}) { + const [orderCriteria, setOrderCriteria] = useState(['']); + + const handleChange = (index, value) => { + const newCriteria = [...orderCriteria]; + newCriteria[index] = value; + + // Si le dernier critère est rempli, on en ajoute un nouveau + if (index === orderCriteria.length - 1 && value !== '') { + newCriteria.push(''); + } + // Si un critère (sauf le premier) est réinitialisé, on le supprime + else if (value === '' && (index !== 0 || orderCriteria.length > 1)) { + newCriteria.splice(index, 1); + } + + setOrderCriteria(newCriteria); + onOrderChange(newCriteria.filter(c => c !== '')); + }; + + // 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'}, + ]; + + if (source === "admin") { + allOptions.push( + {value: 'club.name n', label: 'Club ↓', base: 'club.name'}, + {value: 'club.name i', label: 'Club ↑', base: 'club.name'}, + ); + } + + return ( +
    + {orderCriteria.map((criteria, index) => { + // Récupère les bases des critères déjà sélectionnés (sauf le courant) + const usedBases = orderCriteria + .filter((c, i) => c !== '' && i !== index) + .map(c => allOptions.find(o => o.value === c)?.base); + + // Filtre les options disponibles + const availableOptions = allOptions.filter(option => + !usedBases.includes(option.base) || option.value === criteria + ); + + return ( + + ); + })} +
    + ); +} + let allClub = [] -function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) { +function FiltreBar({ + showLicenceState, + setShowLicenceState, + data, + clubFilter, + setClubFilter, + source, + stateFilter, + setStateFilter, + paymentFilter, + setPaymentFilter + }) { useEffect(() => { if (!data) return; -- 2.49.0 From bf75d9d03691a580bda40e15ec9b825a58712a11 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 18 Nov 2025 13:27:49 +0100 Subject: [PATCH 27/59] feat: make user-friendly cat name --- .../src/components/MemberCustomFiels.jsx | 4 +- src/main/webapp/src/pages/MemberList.jsx | 43 +++++++++++++------ src/main/webapp/src/utils/Tools.js | 29 +++++++++++++ 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/src/components/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx index 0fd9602..abe62ab 100644 --- a/src/main/webapp/src/components/MemberCustomFiels.jsx +++ b/src/main/webapp/src/components/MemberCustomFiels.jsx @@ -1,5 +1,5 @@ import {useEffect, useState} from "react"; -import {getCategoryFormBirthDate} from "../utils/Tools.js"; +import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js"; import {useCountries} from "../hooks/useCountries.jsx"; export function BirthDayField({inti_date, inti_category, required = true}) { @@ -27,7 +27,7 @@ export function BirthDayField({inti_date, inti_category, required = true}) {
    Catégorie {canUpdate && } diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx index 8b97c97..51b3093 100644 --- a/src/main/webapp/src/pages/MemberList.jsx +++ b/src/main/webapp/src/pages/MemberList.jsx @@ -6,7 +6,7 @@ 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} from "../utils/Tools.js"; +import {apiAxios, errFormater, getCatName} from "../utils/Tools.js"; import {toast} from "react-toastify"; import {SearchBar} from "../components/SearchBar.jsx"; import * as XLSX from "xlsx-js-style"; @@ -387,28 +387,45 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page function MakeRow({member, showLicenceState, navigate, source}) { const rowContent = <> -
    +
    {(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "} {(showLicenceState && member.licence != null && member.licence.pay) ? : <>  }
    {member.fname} {member.lname}
    - {source === "club" ? - {member.categorie} - : {member.club?.name || "Sans club"}} +
    + {source === "club" ? + {getCatName(member.categorie)} + :
    {member.club?.name || "Sans club"}
    {getCatName(member.categorie)}
    } +
    + if (showLicenceState && member.licence != null) { - return
    1 ? "warning" : "danger"))} - onClick={() => navigate("" + member.id)}>{rowContent}
    - } else { - return + + } else { + return { + e.preventDefault(); + navigate("" + member.id) + }} + href={"member/" + member.id}> + {rowContent} + } } diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index b7f2314..bd6d6c3 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -60,3 +60,32 @@ export function getSaison(currentDate = new Date()) { return currentDate.getFullYear() - 1 } } + +export function getCatName(cat) { + switch (cat) { + case "SUPER_MINI": + return "Super Mini"; + case "MINI_POUSSIN": + return "Mini Poussin"; + case "POUSSIN": + return "Poussin"; + case "BENJAMIN": + return "Benjamin"; + case "MINIME": + return "Minime"; + case "CADET": + return "Cadet"; + case "JUNIOR": + return "Junior"; + case "SENIOR1": + return "Senior 1"; + case "SENIOR2": + return "Senior 2"; + case "VETERAN1": + return "Vétéran 1"; + case "VETERAN2": + return "Vétéran 2"; + default: + return cat; + } +} -- 2.49.0 From 1b5bf8ba6cb60782e61bb7a7b3a6a4d6b637ea78 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Tue, 18 Nov 2025 14:02:02 +0100 Subject: [PATCH 28/59] feat: add categorie filter membre search --- .../ffsaf/domain/service/MembreService.java | 26 +++++++++++---- .../ffsaf/rest/MembreAdminEndpoints.java | 3 +- .../ffsaf/rest/MembreClubEndpoints.java | 3 +- src/main/webapp/src/pages/MemberList.jsx | 32 ++++++++++--------- src/main/webapp/src/utils/Tools.js | 14 ++++++++ 5 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index bfeccf3..7011e37 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -126,11 +126,17 @@ public class MembreService { } public Uni> searchAdmin(int limit, int page, String search, String club, - int licenceRequest, int payState, String order) { + int licenceRequest, int payState, String order, String categorie) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; + String categorieFilter; + if (categorie == null || categorie.isBlank()) + categorieFilter = " True"; + else + categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal(); + String finalSearch = search; Uni> baseUni = getLicenceListe(licenceRequest, payState); @@ -147,18 +153,18 @@ public class MembreService { if (club == null || club.isBlank()) { query = repository.find( - "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ")", + "id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, sort, finalSearch, ids) .page(Page.ofSize(limit)); } else { if (club.equals("null")) { query = repository.find( - "id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ")", + "id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, sort, finalSearch, ids).page(Page.ofSize(limit)); } else { query = repository.find( - "id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ")", - sort, finalSearch, club + "%", ids) + "id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, + sort, finalSearch, club, ids) .page(Page.ofSize(limit)); } } @@ -167,7 +173,7 @@ public class MembreService { } public Uni> search(int limit, int page, String search, int licenceRequest, int payState, - String order, String subject) { + String order, String categorie, String subject) { if (search == null) search = ""; search = "%" + search.replaceAll(" ", "% %") + "%"; @@ -176,6 +182,12 @@ public class MembreService { Uni> baseUni = getLicenceListe(licenceRequest, payState); + String categorieFilter; + if (categorie == null || categorie.isBlank()) + categorieFilter = " True"; + else + categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal(); + Sort sort = getSort(order); if (sort == null) return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie")); @@ -188,7 +200,7 @@ public class MembreService { return repository.find("userId = ?1", subject).firstResult() .chain(membreModel -> { PanacheQuery query = repository.find( - "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ")", + "id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter, sort, finalSearch, membreModel.getClub(), ids) .page(Page.ofSize(limit)); return getPageResult(query, limit, page); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java index 426ef6e..543dcd7 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreAdminEndpoints.java @@ -58,6 +58,7 @@ public class MembreAdminEndpoints { @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Text à rechercher") @QueryParam("search") String search, @Parameter(description = "Club à filter") @QueryParam("club") String club, + @Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie, @Parameter(description = "État de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, @Parameter(description = "État du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment, @Parameter(description = "Ordre") @QueryParam("order") String order) { @@ -65,7 +66,7 @@ public class MembreAdminEndpoints { limit = 50; if (page == null || page < 1) page = 1; - return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order); + return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order, categorie); } @GET diff --git a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java index d227cda..6ddca01 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/MembreClubEndpoints.java @@ -50,6 +50,7 @@ public class MembreClubEndpoints { @Parameter(description = "Nombre max de résulta (max 50)") @QueryParam("limit") Integer limit, @Parameter(description = "Page à consulter") @QueryParam("page") Integer page, @Parameter(description = "Text à rechercher") @QueryParam("search") String search, + @Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie, @Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest, @Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment, @Parameter(description = "Ordre") @QueryParam("order") String order) { @@ -57,7 +58,7 @@ public class MembreClubEndpoints { limit = 50; if (page == null || page < 1) page = 1; - return membreService.search(limit, page - 1, search, licenceRequest, payment, order, securityCtx.getSubject()); + return membreService.search(limit, page - 1, search, licenceRequest, payment, order, categorie, securityCtx.getSubject()); } @GET diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx index 51b3093..bf55622 100644 --- a/src/main/webapp/src/pages/MemberList.jsx +++ b/src/main/webapp/src/pages/MemberList.jsx @@ -28,18 +28,19 @@ export function MemberList({source}) { const [lastSearch, setLastSearch] = useState(""); const [paymentFilter, setPaymentFilter] = useState(2); const [order, setOrder] = useState(""); + const [catFilter, setCatFilter] = useState(""); const setLoading = useLoadingSwitcher() const { data, error, refresh - } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&order=${order}`, setLoading, 1) + } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&order=${order}&categorie=${catFilter}`, setLoading, 1) useEffect(() => { - refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&order=${order}`); - }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, order]); + refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&order=${order}&categorie=${catFilter}`); + }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, order, catFilter]); useEffect(() => { if (!data) @@ -123,10 +124,10 @@ export function MemberList({source}) {
    Filtre
    - + setPaymentFilter={setPaymentFilter} catFilter={catFilter} setCatFilter={setCatFilter}/>
    @@ -501,31 +502,32 @@ function OrderBar({onOrderChange, source}) { ); } -let allClub = [] - function FiltreBar({ showLicenceState, setShowLicenceState, - data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, - setPaymentFilter + setPaymentFilter, + catFilter, + setCatFilter, }) { - useEffect(() => { - if (!data) - return; - allClub.push(...data.result.map((e) => e.club?.name)) - allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() - }, [data]); return
    +
    + +
    {source !== "club" && }
    setStateId(e.target.value)}/>
    -
    + -