feat: improve result page design

This commit is contained in:
Thibaut Valentin 2026-01-02 13:48:36 +01:00
parent 71a4d98434
commit 82231744af
4 changed files with 225 additions and 84 deletions

View File

@ -18,7 +18,6 @@ import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream; import java.util.stream.Stream;
@WithSession @WithSession
@ -113,26 +112,17 @@ public class ResultService {
}); });
} }
public Uni<List<ResultCategoryData>> getCategory(String uuid, SecurityCtx securityCtx) { public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) {
AtomicReference<MembreModel> membreModel = new AtomicReference<>(); return hasAccess(uuid, securityCtx).chain(r ->
return hasAccess(uuid, securityCtx) matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.invoke(m -> membreModel.set(m.getMembre())) .call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
.chain(m -> matchRepository.list("category.compet.uuid = ?1", uuid)) .map(list -> getData(list, r.getMembre())));
.map(matchModels -> { }
HashMap<Long, List<MatchModel>> 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(); public Uni<ResultCategoryData> getCategory(String uuid, long poule) {
}) return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.onItem() .call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
.transformToMulti(Multi.createFrom()::iterable) .map(list -> getData(list, null));
.onItem().call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
.onItem().transform(list -> getData(list, membreModel.get()))
.collect().asList();
} }
private ResultCategoryData getData(List<MatchModel> matchModels, MembreModel membreModel) { private ResultCategoryData getData(List<MatchModel> matchModels, MembreModel membreModel) {
@ -235,12 +225,6 @@ public class ResultService {
} }
} }
public Uni<ResultCategoryData> getCategoryPublic(String uuid, long poule) {
return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
.map(list -> getData(list, null));
}
private void getTree(List<TreeModel> treeModels, MembreModel membreModel, ResultCategoryData out) { private void getTree(List<TreeModel> treeModels, MembreModel membreModel, ResultCategoryData out) {
ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>(); ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> { treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> {
@ -501,17 +485,11 @@ public class ResultService {
); );
} }
public Uni<ClubArrayData> getClubArray(String uuid, SecurityCtx securityCtx) { public Uni<ClubArrayData> getClubArray(String uuid, Long id, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(cm_register -> return hasAccess(uuid, securityCtx).chain(cm_register -> getClubArray2(uuid, id, cm_register.getMembre()));
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, cm_register.getClub2())
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels ->
getClubArray2(cm_register.getClub2().getName(),
registers.stream().map(o -> (CombModel) o.getMembre()).toList(),
matchModels, registers, cm_register.getMembre()))));
} }
public Uni<ClubArrayData> getClubArrayPublic(String uuid, Long id) { public Uni<ClubArrayData> getClubArray2(String uuid, Long id, MembreModel membreModel) {
if (id < 0) { if (id < 0) {
String clubName = getClubTempId(id); String clubName = getClubTempId(id);
if (clubName == null) { if (clubName == null) {
@ -528,7 +506,7 @@ public class ResultService {
"category.compet.uuid = ?1 AND (c1_guest IN ?2 OR c2_guest IN ?2)", uuid, guests) "category.compet.uuid = ?1 AND (c1_guest IN ?2 OR c2_guest IN ?2)", uuid, guests)
.map(matchModels -> .map(matchModels ->
getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(), getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
matchModels, new ArrayList<>(), null))); matchModels, new ArrayList<>(), membreModel)));
} else { } else {
return clubRepository.findById(id).chain(clubModel -> return clubRepository.findById(id).chain(clubModel ->
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
@ -536,7 +514,7 @@ public class ResultService {
.map(matchModels -> .map(matchModels ->
getClubArray2(clubModel.getName(), getClubArray2(clubModel.getName(),
registers.stream().map(o -> (CombModel) o.getMembre()).toList(), registers.stream().map(o -> (CombModel) o.getMembre()).toList(),
matchModels, registers, null)))); matchModels, registers, membreModel))));
} }
} }

View File

@ -37,7 +37,7 @@ public class ExternalResultEndpoints {
return Uni.createFrom().voidItem(); return Uni.createFrom().voidItem();
if (updateService.needUpdate(poule, rf)) { if (updateService.needUpdate(poule, rf)) {
return resultService.getCategoryPublic(id, poule); return resultService.getCategory(id, poule);
} else { } else {
return Uni.createFrom().voidItem(); return Uni.createFrom().voidItem();
} }
@ -67,7 +67,6 @@ public class ExternalResultEndpoints {
} }
@GET @GET
@Path("/club/list") @Path("/club/list")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -81,6 +80,6 @@ public class ExternalResultEndpoints {
public Uni<?> getClubArray(@QueryParam("club") long club) { public Uni<?> getClubArray(@QueryParam("club") long club) {
if (club == 0) if (club == 0)
return Uni.createFrom().item(""); return Uni.createFrom().item("");
return resultService.getClubArrayPublic(id, club); return resultService.getClubArray2(id, club, null);
} }
} }

View File

@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.ResultService; import fr.titionfire.ffsaf.domain.service.ResultService;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData; import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import fr.titionfire.ffsaf.utils.SecurityCtx; import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.quarkus.security.Authenticated; import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
@ -10,6 +11,7 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import java.util.HashMap;
import java.util.List; import java.util.List;
@Authenticated @Authenticated
@ -29,17 +31,41 @@ public class ResultEndpoints {
} }
@GET @GET
@Path("{uuid}") @Path("{uuid}/category/list")
public Uni<List<ResultCategoryData>> getCategory(@PathParam("uuid") String uuid) { public Uni<HashMap<String, Long>> getCategoryList(@PathParam("uuid") String uuid) {
return resultService.getCategory(uuid, securityCtx); return resultService.getCategoryList(uuid);
} }
@GET @GET
@Path("{uuid}/club") @Path("{uuid}/category/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid) { public Uni<ResultCategoryData> getCategory(@PathParam("uuid") String uuid, @PathParam("id") long id) {
return resultService.getClubArray(uuid, securityCtx); return resultService.getCategory(uuid, id, securityCtx);
} }
@GET
@Path("{uuid}/club/list")
public Uni<HashMap<String, Long>> getClubList(@PathParam("uuid") String uuid) {
return resultService.getClubList(uuid);
}
@GET
@Path("{uuid}/club/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {
return resultService.getClubArray(uuid, id, securityCtx);
}
@GET
@Path("{uuid}/comb/list")
public Uni<HashMap<String, String>> getCombList(@PathParam("uuid") String uuid) {
return resultService.getCombList(uuid, ResultPrivacy.REGISTERED_ONLY);
}
@GET
@Path("{uuid}/comb/{id}")
public Uni<?> getCombList(@PathParam("uuid") String uuid, @PathParam("id") String id) {
return resultService.getCombArrayPublic(uuid, id, ResultPrivacy.REGISTERED_ONLY);
}
@GET @GET
@Path("{uuid}/comb") @Path("{uuid}/comb")
public Uni<ResultService.CombsArrayData> getComb(@PathParam("uuid") String uuid) { public Uni<ResultService.CombsArrayData> getComb(@PathParam("uuid") String uuid) {

View File

@ -3,7 +3,7 @@ import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js"; import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx"; import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {DrawGraph} from "./DrawGraph.jsx"; import {DrawGraph} from "./DrawGraph.jsx";
import {TreeNode} from "../../utils/TreeUtils.js"; import {TreeNode} from "../../utils/TreeUtils.js";
import {scoreToString} from "../../utils/CompetitionTools.js"; import {scoreToString} from "../../utils/CompetitionTools.js";
@ -17,50 +17,45 @@ function CupImg() {
export function ResultView() { export function ResultView() {
const {uuid} = useParams() const {uuid} = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const [resultShow, setResultShow] = useState(null) const [resultShow, setResultShow] = useState("cat")
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}`, setLoading, 1)
return <> return <>
<button type="button" className="btn btn-link" onClick={() => navigate("/result")}> <button type="button" className="btn btn-link" onClick={() => navigate("/result")}>
&laquo; retour &laquo; retour
</button> </button>
{data ? <MenuBar data={data} resultShow={resultShow} setResultShow={setResultShow}/> <MenuBar resultShow={resultShow} setResultShow={setResultShow}/>
: error
? <AxiosError error={error}/>
: <Def/>}
<div className="row"> <div className="row" style={{marginTop: "1em"}}>
<div className="col-auto"> <div className="col-auto">
{resultShow && resultShow.type !== undefined && <PouleResult data={resultShow}/> {resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/>
|| resultShow && resultShow === "club" && <ClubResult uuid={uuid}/> || resultShow && resultShow === "club" && <ClubList uuid={uuid}/>
|| resultShow && resultShow === "comb" && <CombResult uuid={uuid}/>} || resultShow && resultShow === "comb" && <CombList uuid={uuid}/>
|| resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>}
</div> </div>
</div> </div>
</> </>
} }
// || resultShow && resultShow === "club_all" && <ClubAllResult uuid={uuid}/> // || resultShow && resultShow === "club_all" && <ClubAllResult uuid={uuid}/>
function MenuBar({data, resultShow, setResultShow}) { function MenuBar({resultShow, setResultShow}) {
return <ul className="nav nav-tabs"> return <ul className="nav nav-tabs">
<li className="nav-item dropdown"> <li className="nav-item">
<a className={"nav-link dropdown-toggle my-1"} data-bs-toggle="dropdown" href="#" <a className={"nav-link my-1" + (resultShow === "cat" ? " active" : "")} aria-current={(resultShow === "cat" ? " page" : "false")}
aria-current={(resultShow?.type !== undefined ? " page" : "false")} role="button" aria-expanded="false">Catégorie</a> href="#" onClick={_ => setResultShow("cat")}>Par catégorie</a>
<ul className="dropdown-menu">
{data.map(item => <li><a key={item.name} className={"dropdown-item" + (resultShow === item ? " active" : "")}
aria-current={(resultShow === item ? " page" : "false")} href="#" data-bs-toggle="collapse"
onClick={_ => setResultShow(item)}>{item.name}</a></li>)}
</ul>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "club" ? " active" : "")} aria-current={(resultShow === "club" ? " page" : "false")} <a className={"nav-link my-1" + (resultShow === "club" ? " active" : "")} aria-current={(resultShow === "club" ? " page" : "false")}
href="#" onClick={_ => setResultShow("club")}>Mon club</a> href="#" onClick={_ => setResultShow("club")}>Par club</a>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "comb" ? " active" : "")} aria-current={(resultShow === "comb" ? " page" : "false")} <a className={"nav-link my-1" + (resultShow === "comb" ? " active" : "")} aria-current={(resultShow === "comb" ? " page" : "false")}
href="#" onClick={_ => setResultShow("comb")}>Combattants</a> href="#" onClick={_ => setResultShow("comb")}>Par combattant</a>
</li>
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("combs")}>Combattants</a>
</li> </li>
</ul> </ul>
@ -148,13 +143,51 @@ function BuildTree({treeData}) {
return <DrawGraph root={initTree(treeData)}/> return <DrawGraph root={initTree(treeData)}/>
} }
function PouleResult({data}) { function CategoryList({uuid}) {
const [type, setType] = useState(data.type === 3 ? 1 : data.type) const [catId, setCatId] = useState(null)
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/category/list`, setLoading, 1)
useEffect(() => { useEffect(() => {
setType(data.type === 3 ? 1 : data.type) if (data && Object.keys(data).length > 0)
setCatId(data[Object.keys(data).sort()[0]])
}, [data]); }, [data]);
return <>
{data ? <div className="input-group" style={{marginBottom: "1em"}}>
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
<select className="form-select" aria-label="Select Result Type" onChange={e => setCatId(e.target.value)}>
{Object.keys(data).sort().map(key => <option key={key} value={data[key]}>{key}</option>)}
</select>
</div>
: error
? <AxiosError error={error}/>
: <Def/>}
{catId && <CategoryResult uuid={uuid} catId={catId}/>}
</>
}
function CategoryResult({uuid, catId}) {
const [type, setType] = useState(1)
const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/result/${uuid}/category/${catId}`, setLoading, 1)
useEffect(() => {
refresh(`/result/${uuid}/category/${catId}`)
}, [catId]);
useEffect(() => {
if (data)
setType(data.type === 3 ? 1 : data.type)
}, [data]);
if (!data) {
return error
? <AxiosError error={error}/>
: <Def/>
}
return <> return <>
{data.type === 3 && <> {data.type === 3 && <>
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
@ -184,9 +217,37 @@ function PouleResult({data}) {
</> </>
} }
function ClubResult({uuid}) { function ClubList({uuid}) {
const [clubId, setClubId] = useState(null)
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/club`, setLoading, 1) const {data, error} = useFetch(`/result/${uuid}/club/list`, setLoading, 1)
useEffect(() => {
if (data && Object.keys(data).length > 0)
setClubId(data[Object.keys(data).sort()[0]])
}, [data]);
return <>
{data ? <div className="input-group" style={{marginBottom: "1em"}}>
<h6 style={{margin: "auto 0.5em auto 0"}}>Club</h6>
<select className="form-select" aria-label="Select Result Type" onChange={e => setClubId(e.target.value)}>
{Object.keys(data).sort().map(key => <option key={key} value={data[key]}>{key}</option>)}
</select>
</div>
: error
? <AxiosError error={error}/>
: <Def/>}
{clubId && <ClubResult uuid={uuid} clubId={clubId}/>}
</>
}
function ClubResult({uuid, clubId}) {
const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/result/${uuid}/club/${clubId}`, setLoading, 1)
useEffect(() => {
refresh(`/result/${uuid}/club/${clubId}`)
}, [clubId]);
return <> return <>
{data ? <> {data ? <>
@ -242,25 +303,102 @@ function ClubResult({uuid}) {
</> </>
} }
/*function ClubAllResult({uuid}) { function CombList({uuid}) {
return <div></div> const [combId, setCombId] = useState(null)
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/comb/list`, setLoading, 1)
}*/ useEffect(() => {
if (data && Object.keys(data).length > 0)
setCombId(data[Object.keys(data).sort()[0]])
}, [data]);
function CombResult({uuid}) { return <>
{data ? <div className="input-group" style={{marginBottom: "1em"}}>
<h6 style={{margin: "auto 0.5em auto 0"}}>Combattant</h6>
<select className="form-select" aria-label="Select Result Type" onChange={e => setCombId(e.target.value)}>
{Object.keys(data).sort().map(key => <option key={key} value={data[key]}>{key}</option>)}
</select>
</div>
: error
? <AxiosError error={error}/>
: <Def/>}
{combId && <CombResult uuid={uuid} combId={combId}/>}
</>
}
function CombResult({uuid, combId}) {
const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/result/${uuid}/comb/${combId}`, setLoading, 1)
useEffect(() => {
refresh(`/result/${uuid}/comb/${combId}`)
}, [combId]);
if (!data) {
return error
? <AxiosError error={error}/>
: <Def/>
}
return <div>
<h3>Info :</h3>
<ul>
<li>Nom Prénom : {data.name}</li>
<li>Club : {data.club}</li>
<li>Catégorie : {data.cat}</li>
</ul>
<h3>Statistique :</h3>
<ul>
<li>Taux de victoire : {data.matchs.length === 0 ? "---" : (data.totalWin / data.matchs.length * 100).toFixed(0)}% ({data.totalWin} sur
${data.matchs.length})
</li>
<li>Points marqués : {data.pointMake}</li>
<li>Points reçus : {data.pointTake}</li>
<li>Ratio du score (point marqué / point reçu): {data.pointRatio.toFixed(3)}</li>
</ul>
<h3>Liste des matchs:</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>Catégorie</th>
<th scope="col" style={{textAlign: "center"}}>Adversaire</th>
<th scope="col" style={{textAlign: "center"}}>Scores</th>
<th scope="col" style={{textAlign: "center"}}>Ratio</th>
<th scope="col" style={{textAlign: "center"}}></th>
</tr>
</thead>
<tbody>
{data.matchs.map((match, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{match.poule}</td>
<td style={{textAlign: "center"}}>{match.adv}</td>
<td style={{textAlign: "center"}}>{scoreToString(match.score)}</td>
<td style={{textAlign: "center"}}>{match.ratio.toFixed(3)}</td>
<td style={{textAlign: "left"}}>{match.win ? <CupImg/> : ""}</td>
</tr>)}
</tbody>
</table>
</div>
}
function CombsResult({uuid}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1) const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1)
return <> return <>
{data ? <> {data ? <>
<h3>Statistique :</h3> <h3>Statistique :</h3>
<ul> <ul>
<li>Nombre d'inscris : {data.nb_insc}</li> <li>Nombre d'inscris : {data.nb_insc}</li>
<li>Nombre de match disput&eacute; : {data.tt_match}</li> <li>Nombre de match disput&eacute; : {data.tt_match}</li>
<li>Points marqués : {data.point}</li> <li>Points marqués : {data.point}</li>
</ul> </ul>
<h3>Liste des combattants :</h3> <h3>Liste des combattants :</h3>
<table className="table table-striped"> <table className="table table-striped">
<thead> <thead>