feat: classement club

This commit is contained in:
Thibaut Valentin 2026-02-23 11:58:39 +01:00
parent 2f390b03e2
commit 4984a09803
7 changed files with 230 additions and 5 deletions

View File

@ -7,6 +7,8 @@ import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*; import fr.titionfire.ffsaf.utils.*;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Multi;
@ -15,6 +17,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import lombok.Builder; import lombok.Builder;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*; import java.util.*;
@ -52,6 +55,10 @@ public class ResultService {
@Inject @Inject
TradService trad; TradService trad;
@Inject
@CacheName("club-classement")
Cache cacheClubClassement;
private static final HashMap<Long, String> combTempIds = new HashMap<>(); private static final HashMap<Long, String> combTempIds = new HashMap<>();
private static String getCombTempId(Long key) { private static String getCombTempId(Long key) {
@ -214,7 +221,7 @@ public class ResultService {
} }
public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards, public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards,
ResultCategoryData out) { ResultCategoryData out) {
if ((categoryModel.getType() & 2) != 0) { if ((categoryModel.getType() & 2) != 0) {
AtomicInteger rank = new AtomicInteger(0); AtomicInteger rank = new AtomicInteger(0);
categoryModel.getTree().stream() categoryModel.getTree().stream()
@ -231,7 +238,8 @@ public class ResultService {
} else { } else {
for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) { for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) {
for (ResultCategoryData.RankArray r : list) { for (ResultCategoryData.RankArray r : list) {
out.getClassement().add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName())); out.getClassement()
.add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName()));
} }
} }
} }
@ -724,6 +732,100 @@ public class ResultService {
} }
} }
public Uni<List<ClubClassement>> getAllClubArray(String uuid) {
return getAllClubArray(uuid, true);
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> getAllClubArray(uuid, true));
}
public Uni<List<ClubClassement>> getAllClubArray(String uuid, boolean cache) {
List<CardModel> cards = new java.util.ArrayList<>();
//noinspection unchecked
cacheClubClassement.invalidateIf(
(p) -> ((Pair<Long, List<ClubClassement>>) p).getKey() > System.currentTimeMillis());
if (!cache)
cacheClubClassement.invalidate(uuid);
return cacheClubClassement.getAsync(uuid, k -> cardRepository.list("competition.uuid = ?1", uuid)
.invoke(__ -> System.out.println("Cache miss for club classement with uuid " + uuid))
.invoke(cards::addAll)
.chain(__ -> matchRepository.list("category.compet.uuid = ?1", uuid))
.chain(matchs -> {
HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
for (MatchModel match : matchs) {
if (!map.containsKey(match.getCategory()))
map.put(match.getCategory(), new java.util.ArrayList<>());
map.get(match.getCategory()).add(match);
}
return Multi.createFrom().iterable(map.entrySet())
.onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
.map(entry -> {
ResultCategoryData tmp = new ResultCategoryData();
getArray2(entry.getValue().stream().map(m -> new MatchModelExtend(m, cards)).toList(),
null, tmp);
getClassementArray(entry.getKey(), null, cards, tmp);
return tmp;
})
.collect().asList();
})
.map(categoryData -> {
HashMap<Long, ClubClassement> clubMap = new HashMap<>();
categoryData.forEach(c -> c.getClassement().forEach(classementData -> {
if (classementData.rank() > 3)
return;
if (classementData.comb() != null) {
long clubId = 0L;
String clubName = "";
if (classementData.comb() instanceof MembreModel membreModel2) {
clubId = (membreModel2.getClub() != null) ? membreModel2.getClub().getId() : 0;
clubName = (membreModel2.getClub() != null) ? membreModel2.getClub().getName() : "";
} else if (classementData.comb() instanceof CompetitionGuestModel guestModel) {
if (guestModel.getClub() != null && !guestModel.getClub().isBlank())
clubId = getClubTempId(guestModel.getClub());
clubName = guestModel.getClub();
}
if (clubId != 0) {
clubMap.putIfAbsent(clubId, new ClubClassement(clubName));
ClubClassement entity = clubMap.get(clubId);
entity.score[classementData.rank() - 1]++;
entity.tt_score += 4 - classementData.rank();
}
}
}));
return clubMap.values().stream()
.sorted(Comparator.comparingInt((ClubClassement c) -> c.tt_score)
.thenComparingInt(c -> c.score[0])
.thenComparingInt(c -> c.score[1])
.thenComparingInt(c -> c.score[2]).reversed())
.toList();
})
.map(l -> new Pair<>(System.currentTimeMillis() + 60 * 1000L, l))
).map(Pair::getValue);
}
@Data
@RegisterForReflection
public static class ClubClassement {
String name;
Integer[] score = new Integer[]{0, 0, 0};
int tt_score = 0;
public ClubClassement(String name) {
this.name = name;
}
}
@RegisterForReflection @RegisterForReflection
public static class CombStat { public static class CombStat {
public int w; public int w;

View File

@ -8,6 +8,7 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
@Path("api/public/result/{id}") @Path("api/public/result/{id}")
public class ExternalResultEndpoints { public class ExternalResultEndpoints {
@ -73,6 +74,13 @@ public class ExternalResultEndpoints {
return resultService.getClubList(id); return resultService.getClubList(id);
} }
@GET
@Path("/club/classement")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<ResultService.ClubClassement>> clubClassement() {
return resultService.getAllClubArray(id);
}
@GET @GET
@Path("/club/data") @Path("/club/data")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View File

@ -47,6 +47,12 @@ public class ResultEndpoints {
return resultService.getClubList(uuid, securityCtx); return resultService.getClubList(uuid, securityCtx);
} }
@GET
@Path("{uuid}/club/classement")
public Uni<List<ResultService.ClubClassement>> getClubClassement(@PathParam("uuid") String uuid) {
return resultService.getAllClubArray(uuid, securityCtx);
}
@GET @GET
@Path("{uuid}/club/{id}") @Path("{uuid}/club/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) { public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {
@ -64,7 +70,7 @@ public class ResultEndpoints {
public Uni<?> getCombList(@PathParam("uuid") String uuid, @PathParam("id") String id) { public Uni<?> getCombList(@PathParam("uuid") String uuid, @PathParam("id") String id) {
return resultService.getCombArrayPublic(uuid, id, securityCtx); return resultService.getCombArrayPublic(uuid, id, securityCtx);
} }
@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

@ -39,6 +39,9 @@ function setSubPage(name) {
case 'club': case 'club':
clubPage(location); clubPage(location);
break; break;
case 'clubRank':
clubRankPage();
break;
case 'all': case 'all':
combsPage(); combsPage();
break; break;
@ -54,6 +57,7 @@ function homePage() {
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li> <li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li>
<li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li> <li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li>
<li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li> <li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li>
<li><a id="clubClassement" href="javascript:void(0);">${i18next.t('classementClub')}</a></li>
<li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li> <li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
</ul> </ul>
` `
@ -62,6 +66,7 @@ function homePage() {
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule')); document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb')); document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club')); document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
document.getElementById('clubClassement').addEventListener('click', () => setSubPage('clubRank'));
document.getElementById('allLink').addEventListener('click', () => setSubPage('all')); document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
} }
@ -640,6 +645,60 @@ function clubPage(location) {
rootDiv.append(content) rootDiv.append(content)
} }
function buildClubsView(clubs) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>${i18next.t('classementClub')} :</h3>
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
<table style="width: 1200px;overflow: auto"><thead>
<tr>
<th class="has-text-align-center" data-align="center">${i18next.t('club')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('1er')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('2eme')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('3eme')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
</tr>
</thead><tbody>`
for (const club of clubs) {
arrayContent += `
<tr>
<td class="has-text-align-center" data-align="center">${club.name}</td>
<td class="has-text-align-center" data-align="center">${club.score[0]}</td>
<td class="has-text-align-center" data-align="center">${club.score[1]}</td>
<td class="has-text-align-center" data-align="center">${club.score[2]}</td>
<td class="has-text-align-center" data-align="center">${club.tt_score}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
pouleDiv.innerHTML = arrayContent;
return pouleDiv;
}
function clubRankPage() {
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
const content = document.createElement('div');
content.style.marginTop = '1em';
const dataContainer = document.createElement('div');
dataContainer.id = 'data-container';
const loading = startLoading(content);
fetch(`${apiUrlRoot}/club/classement`)
.then(response => response.json())
.then(clubs => {
console.log(clubs);
dataContainer.replaceChildren(buildClubsView(clubs));
})
.catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaListe'))))
.finally(() => stopLoading(loading));
content.append(dataContainer);
rootDiv.append(content)
}
function buildCombsView(combs) { function buildCombsView(combs) {
const pouleDiv = document.createElement('div'); const pouleDiv = document.createElement('div');
let arrayContent = ` let arrayContent = `

View File

@ -2,6 +2,9 @@
"--sélectionnerUnClub--": "--Select a club--", "--sélectionnerUnClub--": "--Select a club--",
"--sélectionnerUnCombattant--": "--Select a fighter--", "--sélectionnerUnCombattant--": "--Select a fighter--",
"--sélectionnerUneCatégorie--": "--Select a category--", "--sélectionnerUneCatégorie--": "--Select a category--",
"1er": "Gold medal",
"2eme": "Silver medal",
"3eme": "Bronze medal",
"abs.": "abs.", "abs.": "abs.",
"adversaire": "Opponent", "adversaire": "Opponent",
"aujourdhuià": "Today at {{time}}", "aujourdhuià": "Today at {{time}}",
@ -11,7 +14,8 @@
"catégorie": "Category", "catégorie": "Category",
"chargement": "Loading", "chargement": "Loading",
"classement": "Ranking", "classement": "Ranking",
"classementFinal": "Final standings", "classementClub": "Club ranking",
"classementFinal": "Final ranking",
"club": "Club", "club": "Club",
"combattant": "Fighter", "combattant": "Fighter",
"combattants": "Fighters", "combattants": "Fighters",

View File

@ -2,6 +2,9 @@
"--sélectionnerUnClub--": "--Sélectionner un club--", "--sélectionnerUnClub--": "--Sélectionner un club--",
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--", "--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
"--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--", "--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--",
"1er": "Médaille d'or",
"2eme": "Médaille d'argent",
"3eme": "Médaille de bronze",
"abs.": "abs.", "abs.": "abs.",
"adversaire": "Adversaire", "adversaire": "Adversaire",
"aujourdhuià": "Aujourd'hui à {{time}}", "aujourdhuià": "Aujourd'hui à {{time}}",
@ -11,6 +14,7 @@
"catégorie": "Catégorie", "catégorie": "Catégorie",
"chargement": "Chargement", "chargement": "Chargement",
"classement": "Classement", "classement": "Classement",
"classementClub": "Classement club",
"classementFinal": "Classement final", "classementFinal": "Classement final",
"club": "Club", "club": "Club",
"combattant": "Combattant", "combattant": "Combattant",

View File

@ -39,7 +39,8 @@ export function ResultView() {
{resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/> {resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/>
|| resultShow && resultShow === "club" && <ClubList uuid={uuid}/> || resultShow && resultShow === "club" && <ClubList uuid={uuid}/>
|| resultShow && resultShow === "comb" && <CombList uuid={uuid}/> || resultShow && resultShow === "comb" && <CombList uuid={uuid}/>
|| resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>} || resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>
|| resultShow && resultShow === "clubs" && <ClubsResult uuid={uuid}/>}
</div> </div>
</div> </div>
</> </>
@ -66,6 +67,10 @@ function MenuBar({resultShow, setResultShow}) {
<a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")} <a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a> href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a>
</li> </li>
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "clubs" ? " active" : "")} aria-current={(resultShow === "clubs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("clubs")}>{t('classementClub')}</a>
</li>
</ul> </ul>
/* /*
@ -450,6 +455,43 @@ function CombResult({uuid, combId}) {
</div> </div>
} }
function ClubsResult({uuid}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/club/classement`, setLoading, 1)
const {t} = useTranslation('result');
return <>
{data ? <>
<h3>{t('classementClub')} :</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>{t('club')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('1er')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('2eme')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('3eme')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('scores')}</th>
</tr>
</thead>
<tbody>
{data.map((club, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{club.name}</td>
<td style={{textAlign: "center"}}>{club.score[0]}</td>
<td style={{textAlign: "center"}}>{club.score[1]}</td>
<td style={{textAlign: "center"}}>{club.score[2]}</td>
<td style={{textAlign: "center"}}>{club.tt_score}</td>
</tr>)}
</tbody>
</table>
</>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function CombsResult({uuid}) { 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)