9 Commits

34 changed files with 1362 additions and 1906 deletions

View File

@@ -122,9 +122,7 @@ public class LicenceService {
.call(genLicenceNumberAndAccountIfNeed())
: Uni.createFrom().nullItem()
))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD,
"%s (valid=%b, pay=%b, %s)" .formatted(membreModel.getObjectName(), model.isValidate(),
model.isPay(), model.getCertificate()),
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
licenceModel));
});
} else {

View File

@@ -184,17 +184,11 @@ public class MembreService {
String finalSearch = search;
return getLicenceListe(licenceRequest, payState)
.chain(l -> {
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> {
PanacheQuery<MembreModel> query;
query = repository.find(queryStr, sort, finalSearch,
l.stream().map(l2 -> l2.getMembre().getId()).toList(), club).page(Page.ofSize(limit));
return getPageResult(query, limit, page)
.call(r -> (r.getResult().isEmpty() || !l.isEmpty() ?
Uni.createFrom().item(l) :
licenceRepository.list("saison = ?1 AND membre.id IN ?2", Utils.getSaison(),
r.getResult().stream().map(SimpleMembre::getId).toList()))
.invoke(l2 -> r.setAdditionalData(l2.stream().map(SimpleLicence::fromModel)))
);
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit));
return getPageResult(query, limit, page);
});
}
@@ -216,7 +210,7 @@ public class MembreService {
public Uni<List<SimpleMembreInOutData>> getAllExport(String subject) {
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> repository.list("club = ?1", membreModel.getClub()))
.chain(membres -> licenceRepository.list("membre IN ?1", membres)
.chain(membres -> licenceRepository.list("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres)
.map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList()));
}

View File

@@ -7,8 +7,6 @@ import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
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.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi;
@@ -17,7 +15,6 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.Builder;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*;
@@ -55,11 +52,6 @@ public class ResultService {
@Inject
TradService trad;
@Inject
@CacheName("club-classement")
Cache cacheClubClassement;
private static final HashMap<String, Long> cacheClubClassementExp = new HashMap<>();
private static final HashMap<Long, String> combTempIds = new HashMap<>();
private static String getCombTempId(Long key) {
@@ -239,8 +231,7 @@ public class ResultService {
} else {
for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) {
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()));
}
}
}
@@ -616,32 +607,21 @@ public class ResultService {
new ArrayList<>(), membreModel)
)));
} else {
return cardRepository.list("competition.uuid = ?1", uuid)
.chain(cards -> clubRepository.findById(id).chain(clubModel ->
return clubRepository.findById(id).chain(clubModel ->
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
.chain(registers -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
uuid, registers.stream().map(RegisterModel::getMembre).toList())
.map(matchModels -> matchModels.stream()
.map(m -> new MatchModelExtend(m, cards)))
.chain(s -> competitionGuestRepository.list(
"competition.uuid = ?1 AND club = ?2", uuid, clubModel.getName())
.chain(guests -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
uuid, guests)
.map(mm -> new Pair<>(guests, Stream.concat(s, mm.stream()
.map(m -> new MatchModelExtend(m, cards)))
.distinct().toList()))
))
.map(p ->
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards ->
getClubArray2(clubModel.getName(),
Stream.concat(
registers.stream().map(RegisterModel::getMembre),
p.getKey().stream()
).toList(),
p.getValue(), registers, membreModel)
registers.stream().map(o -> (CombModel) o.getMembre())
.toList(),
matchModels.stream()
.map(m -> new MatchModelExtend(m, cards)).toList(),
registers, membreModel)
))));
}
}
@@ -744,112 +724,6 @@ 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<>();
return (!cache || cacheClubClassementExp.getOrDefault(uuid, 0L) < System.currentTimeMillis() ?
cacheClubClassement.invalidate(uuid).invoke(() -> cacheClubClassementExp.remove(uuid)) :
Uni.createFrom().voidItem()
).chain(o -> 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<String, ClubClassement> clubMap = new HashMap<>();
categoryData.forEach(
c -> c.getClassement().stream().map(ResultCategoryData.ClassementData::comb)
.filter(Objects::nonNull)
.distinct()
.map(comb -> {
if (comb instanceof MembreModel membreModel2) {
return (membreModel2.getClub() != null) ? membreModel2.getClub()
.getName() : "";
} else if (comb instanceof CompetitionGuestModel guestModel) {
return guestModel.getClub();
}
return "";
})
.filter(s -> s != null && !s.isBlank() && !s.equals("Team"))
.distinct()
.forEach(clubName -> clubMap.putIfAbsent(clubName,
new ClubClassement(clubName))));
categoryData.forEach(c -> c.getClassement().forEach(classementData -> {
if (classementData.rank() > 3)
return;
if (classementData.comb() != null) {
String clubName = "";
if (classementData.comb() instanceof MembreModel membreModel2) {
clubName = (membreModel2.getClub() != null) ? membreModel2.getClub()
.getName() : "";
} else if (classementData.comb() instanceof CompetitionGuestModel guestModel) {
clubName = guestModel.getClub();
}
if (clubName != null && !clubName.isBlank()
&& !clubName.equals("Team") && clubMap.containsKey(clubName)) {
ClubClassement entity = clubMap.get(clubName);
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();
})
.invoke(__ -> cacheClubClassementExp.put(uuid, System.currentTimeMillis() + 60 * 1000L))
));
}
@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
public static class CombStat {
public int w;

View File

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

View File

@@ -47,12 +47,6 @@ public class ResultEndpoints {
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
@Path("{uuid}/club/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {

View File

@@ -2,14 +2,10 @@ package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
@@ -28,27 +24,7 @@ public class SimpleMembreInOutData {
public static SimpleMembreInOutData fromModel(MembreModel membreModel, List<LicenceModel> lc) {
LicenceModel currentLicence = lc.stream().filter(l -> l.getMembre().getId().equals(membreModel.getId()))
.max(Comparator.comparingInt(LicenceModel::getSaison)).orElse(null);
String certif = null;
if (currentLicence != null && currentLicence.getCertificate() != null) {
String[] strings = currentLicence.getCertificate().split("¤");
if (currentLicence.getSaison() == Utils.getSaison()) {
certif = currentLicence.getCertificate();
} else if (strings.length > 1) {
try {
Date date = new SimpleDateFormat("yyyy-MM-dd").parse(strings[1]);
Calendar max = Utils.getFirstDateOfSaison();
max.add(Calendar.YEAR, -2);
if (max.getTime().compareTo(date) < 0) {
certif = currentLicence.getCertificate();
}
} catch (Exception ignored) {
}
}
}
.findFirst().orElse(null);
return new SimpleMembreInOutData(
membreModel.getLicence(),
@@ -57,8 +33,8 @@ public class SimpleMembreInOutData {
membreModel.getEmail(),
membreModel.getGenre().str,
membreModel.getBirth_date(),
currentLicence != null && currentLicence.getSaison() == Utils.getSaison(),
certif
currentLicence != null,
currentLicence == null ? null : currentLicence.getCertificate()
);
}
}

View File

@@ -10,14 +10,13 @@ import java.util.List;
@Data
@RegisterForReflection
public class PageResult<T> {
@Schema(description = "Le numéro de la page courante.", examples = "1")
@Schema(description = "Le numéro de la page courante.", example = "1")
private int page;
@Schema(description = "Le nombre d'éléments par page.", examples = "10")
@Schema(description = "Le nombre d'éléments par page.", example = "10")
private int page_size;
@Schema(description = "Le nombre total de pages.", examples = "5")
@Schema(description = "Le nombre total de pages.", example = "5")
private int page_count;
@Schema(description = "Le nombre total d'éléments.", examples = "47")
@Schema(description = "Le nombre total d'éléments.", example = "47")
private long result_count;
private List<T> result = new ArrayList<>();
private Object additionalData;
}

View File

@@ -20,8 +20,6 @@ import java.io.*;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
@@ -119,15 +117,6 @@ public class Utils {
}
}
public static Calendar getFirstDateOfSaison() {
Calendar calendar = Calendar.getInstance();
calendar.set(getSaison(), Calendar.SEPTEMBER, 1, 0, 0, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
public static Calendar toCalendar(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
@@ -255,16 +244,15 @@ public class Utils {
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter);
if (files != null) {
for (File f2 : files) {
for (File f : files) {
//noinspection ResultOfMethodCallIgnored
f2.delete();
f.delete();
}
}
Path f = file.filePath();
System.out.println(f + " -> " + new File(dirFile, id + "." + detectedExtensions[0]));
Files.copy(f, new File(dirFile, id + "." + detectedExtensions[0]).toPath(),
StandardCopyOption.REPLACE_EXISTING);
File f = file.filePath().toFile();
//noinspection ResultOfMethodCallIgnored
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0]));
return "ok";
})));
}

View File

@@ -99,11 +99,6 @@ public class RPDF {
});
}
@WSReceiver(code = "getPodiumClub", permission = PermLevel.VIEW)
public Uni<List<ResultService.ClubClassement>> getPodiumClub(WebSocketConnection connection, Object o) {
return resultService.getAllClubArray(connection.pathParam("uuid"), false);
}
@RegisterForReflection
public static record PodiumEntity(String poule_name, String source, Categorie categorie,
List<ResultCategoryData.ClassementData> podium) {

File diff suppressed because it is too large Load Diff

View File

@@ -13,42 +13,42 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fortawesome/fontawesome-svg-core": "^7.3.1",
"@fortawesome/free-brands-svg-icons": "^7.3.1",
"@fortawesome/free-regular-svg-icons": "^7.3.1",
"@fortawesome/free-solid-svg-icons": "^7.3.1",
"@fortawesome/react-fontawesome": "^3.5.0",
"axios": "1.18.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.1",
"axios": "^1.13.2",
"browser-image-compression": "^2.0.2",
"i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^4.0.1",
"jspdf": "4.2.1",
"jspdf-autotable": "^5.0.8",
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jspdf": "^4.1.0",
"jspdf-autotable": "^5.0.7",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.8",
"proj4": "^2.21.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.11",
"react-is": "^19.2.8",
"obs-websocket-js": "^5.0.7",
"proj4": "^2.20.2",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.3",
"react-is": "^19.2.3",
"react-leaflet": "^5.0.0",
"react-loader-spinner": "^8.0.2",
"react-router-dom": "^7.18.2",
"react-toastify": "^11.1.0",
"recharts": "^3.10.1",
"react-router-dom": "^7.12.0",
"react-toastify": "^11.0.5",
"recharts": "^3.7.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"xlsx-js-style": "^1.2.0"
},
"devDependencies": {
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"eslint": "^9.39.5",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"vite": "8.0.16"
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"vite": "^7.3.1"
}
}

View File

@@ -39,9 +39,6 @@ function setSubPage(name) {
case 'club':
clubPage(location);
break;
case 'clubRank':
clubRankPage();
break;
case 'all':
combsPage();
break;
@@ -57,7 +54,6 @@ function homePage() {
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li>
<li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li>
<li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li>
<li><a id="clubClassement" href="javascript:void(0);">${i18next.t('classementClub')}</a></li>
<li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
</ul>
`
@@ -66,7 +62,6 @@ function homePage() {
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
document.getElementById('clubClassement').addEventListener('click', () => setSubPage('clubRank'));
document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
}
@@ -645,60 +640,6 @@ function clubPage(location) {
rootDiv.append(content)
}
function buildClubsView(clubs) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>${i18next.t('classementDesClub')} :</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) {
const pouleDiv = document.createElement('div');
let arrayContent = `

View File

@@ -120,8 +120,6 @@
"obs.préfixDesSources": "Source prefix",
"pays": "Country",
"personnaliser": "Personalize",
"podium": "Podium",
"podiumDesClubs": "Club podium",
"poids": "Weight",
"poule": "Pool",
"poulePour": "Pool for: ",

View File

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

View File

@@ -120,8 +120,6 @@
"obs.préfixDesSources": "Préfix des sources",
"pays": "Pays",
"personnaliser": "Personnaliser",
"podium": "Podium",
"podiumDesClubs": "Podium des clubs",
"poids": "Poids",
"poule": "Poule",
"poulePour": "Poule pour: ",

View File

@@ -2,9 +2,6 @@
"--sélectionnerUnClub--": "--Sélectionner un club--",
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
"--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.",
"adversaire": "Adversaire",
"aujourdhuià": "Aujourd'hui à {{time}}",
@@ -14,8 +11,6 @@
"catégorie": "Catégorie",
"chargement": "Chargement",
"classement": "Classement",
"classementClub": "Classement club",
"classementDesClub": "Classement des clubs",
"classementFinal": "Classement final",
"club": "Club",
"combattant": "Combattant",

View File

@@ -1,84 +0,0 @@
class ProcessorDTMF extends AudioWorkletProcessor {
constructor() {
super();
this.sampleRate = sampleRate;
this.symbolDuration = 0.03; // 50 ms par symbole
this.samplesPerSymbol = Math.floor(this.sampleRate * this.symbolDuration);
this.encodeLowPrio = [];
this.symbolSamples = [];
this.lastBlackStep = 0;
this.port.onmessage = (e) => {
if (e.data.type === 'encode') {
this.symbolSamples.push(...this.encodeSymbols(e.data.symbols));
this.symbolSamples.push(...this.encodeBlack(this.sampleRate * 0.02));
}
if (e.data.type === 'encodeLowPrio') {
this.encodeLowPrio.push(e.data.symbols);
}
};
}
dtmfFrequencies = [
[697, 770, 852, 941], // Fréquences basses
[1209, 1336, 1477, 1633] // Fréquences hautes
];
encodeSymbols(symbols) {
const samples = [];
for (const symbol of symbols) {
const lf = this.dtmfFrequencies[0][symbol % 4]; // Fréquence basse
const hf = this.dtmfFrequencies[1][Math.floor(symbol / 4)]; // Fréquence haute
// console.log(`Symbol: ${symbol}, LF: ${lf} Hz, HF: ${hf} Hz`);
for (let i = 0; i < this.samplesPerSymbol; i++) {
const t = i / this.sampleRate;
const t2 = (this.lastBlackStep + i) / this.sampleRate;
samples.push(0.5 * Math.sin(2 * Math.PI * lf * t) + 0.5 * Math.sin(2 * Math.PI * hf * t) // Signal DTMF
+ Math.sin(2 * Math.PI * 150 * t2) * (0.0625 * (Math.sin(2 * Math.PI * 0.5 * t2) + 1))); // Ajouter un signal à 150 Hz pour le "black"
}
this.lastBlackStep += this.samplesPerSymbol;
// ajouter un silence de 10 ms entre les symboles
samples.push(...this.encodeBlack(this.sampleRate * 0.01)); // Silence
}
return samples;
}
encodeBlack(size) {
const samples = [];
for (let i = 0; i < size; i++) {
const t = (this.lastBlackStep + i) / this.sampleRate;
samples.push(Math.sin(2 * Math.PI * 150 * t) * (0.0625 * (Math.sin(2 * Math.PI * 0.5 * t) + 1))); // Signal à 350 Hz pour le "black"
}
this.lastBlackStep += size;
this.lastBlackStep %= this.sampleRate * 2; // Réinitialiser tous les 2 secondes pour éviter les débordements
return samples;
}
process(inputs, outputs, parameters) {
const output = outputs[0]; // output est un tableau de canaux (ex: [Float32Array, ...])
const channelData = output[0]; // Accéder au premier canal (mono)
if (this.symbolSamples.length === 0 && this.encodeLowPrio.length > 0) {
this.symbolSamples.push(...this.encodeSymbols(this.encodeLowPrio.shift()));
this.symbolSamples.push(...this.encodeBlack(this.sampleRate * 0.02));
}
if (this.symbolSamples.length === 0) {
const samples = this.encodeBlack(channelData.length)
for (let i = 0; i < channelData.length; i++) {
channelData[i] = samples[i] || 0;
}
return true;
}
for (let i = 0; i < channelData.length; i++) {
channelData[i] = this.symbolSamples.shift() || 0; // Prendre le prochain échantillon ou 0 si vide
}
return true;
}
}
registerProcessor('dtmf-processor', ProcessorDTMF);

View File

@@ -1,207 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import {useTablesState} from "../../pages/competition/editor/StateWindow.jsx";
import {timePrint} from "../../utils/Tools.js";
let initialized = false;
const AudioEncoder = () => {
const audioContextRef = useRef(null);
const qpskProcessorRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const [table, setTable] = useState('1');
const lastSend = useRef({id: 0});
const {state} = useTablesState();
// Initialisation de l'AudioContext et du AudioWorklet
useEffect(() => {
const initAudio = async () => {
if (initialized)
return;
console.log("Initialisation de l'audio après interaction utilisateur");
initialized = true;
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log("AudioContext state:", audioContext.state);
if (audioContext.state === 'suspended') {
await audioContext.resume(); // Nécessaire pour démarrer le contexte audio
console.log("AudioContext resumed");
}
await audioContext.audioWorklet.addModule('/processor-dtmf.js');
await new Promise(r => setTimeout(r, 100));
const processor = new AudioWorkletNode(audioContext, 'dtmf-processor');
processor.connect(audioContext.destination);
qpskProcessorRef.current = processor;
audioContextRef.current = audioContext;
await new Promise(r => setTimeout(r, 100));
setIsReady(true);
} catch (err) {
initialized = false;
console.error("Erreur d'initialisation AudioWorklet:", err);
}
};
// Initialiser après un clic utilisateur (pour contourner les restrictions des navigateurs)
const handleUserInteraction = () => {
document.removeEventListener('click', handleUserInteraction);
initAudio();
};
document.addEventListener('click', handleUserInteraction);
return () => {
if (audioContextRef.current?.state !== 'closed') {
audioContextRef.current?.close();
}
};
}, []);
// Fonction pour encoder et envoyer un message
const encodeAndSend = (data, lowPrio = false) => {
if (!isReady) return;
const symbols = Array.from(data).flatMap(byte => [byte >> 4, byte & 0x0F]);
console.log("Bits :", symbols);
// 5. Envoyer les symboles au processeur audio
if (lowPrio) {
qpskProcessorRef.current.port.postMessage({type: 'encodeLowPrio', symbols});
} else {
qpskProcessorRef.current.port.postMessage({type: 'encode', symbols});
}
}
useEffect(() => {
const t = state.find(o => o.liceName === table)
if (!t)
return
// console.log("Data for table 1:", t, t.selectedMatch)
const last = lastSend.current;
if (t.selectedMatch !== last.id) {
clearTimeout(last.time_id)
last.time_id = setTimeout(() => {
last.time_id = null
if (t.selectedMatch === null) {
encodeAndSend(new Uint8Array([0, 0, 0, 0, 0, 0, 0]));
} else {
const data = [];
for (let i = 0; i < 7; i++) { // MaxSafeInteger est sur 7 bytes (53 bits de précision)
data.unshift(Number((BigInt(t.selectedMatch) >> BigInt(i * 8)) & 0xFFn))
}
data[0] = data[0] & 0x1F // 3 premiers bits à 0 pour différencier des autres types de messages (ex: score, chrono, etc.)
// console.log("Data to send (selectedMatch):", data)
encodeAndSend(new Uint8Array(data), false);
}
}, 250)
last.id = t.selectedMatch
}
const isRunning = (c) => c.startTime !== 0
const getTime = (c) => {
if (c.startTime === 0)
return c.time
return c.time + Date.now() - c.startTime
}
const timeStr = last.chronoState ? timePrint((last.chronoState.state === 2) ? last.chronoState.configPause : last.chronoState.configTime - getTime(last.chronoState)) : "-"
const timeStr2 = timePrint((t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState))
if (timeStr !== timeStr2) {
clearInterval(lastSend.current.time_chronoInter)
clearTimeout(lastSend.current.time_chronoText)
lastSend.current.time_chronoText = setTimeout(() => {
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
const ms = time % 1000
time = (time - ms) / 1000
const data = [((time >> 8) & 0x1F) + 0x20, time & 0xFF];
// console.log("Data to send (time):", data)
encodeAndSend(new Uint8Array(data));
lastSend.current.time_chronoInter = setInterval(() => {
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
const ms = time % 1000
time = (time - ms) / 1000
const data = [((time >> 8) & 0x1F) + 0x20, time & 0xFF];
// console.log("Data to send (time-auto):", data)
encodeAndSend(new Uint8Array(data), true);
}, 10000);
}, 150)
}
if (!last.chronoState || last.chronoState.state !== t.chronoState.state || isRunning(last.chronoState) !== isRunning(t.chronoState)) {
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
const ms = Math.round((time % 1000) / 250)
const data = [0x40 + (t.chronoState.state << 3) + (isRunning(t.chronoState) << 2) + (ms & 0x03)];
// console.log("Data to send (chrono state):", data)
encodeAndSend(new Uint8Array(data));
}
last.chronoState = {...t.chronoState}
// console.log(timeStr, timeStr2)
// console.log(last.chronoState, t.chronoState)
if (last.scoreRouge !== t.scoreState.scoreRouge) {
clearTimeout(last.time_sr)
last.time_sr = setTimeout(() => {
if (last.scoreRouge !== t.scoreState.scoreRouge) {
const b = t.scoreState.scoreRouge < 0
const s = b ? -t.scoreState.scoreRouge : t.scoreState.scoreRouge
const data = [0x60 + (b << 3) + ((s >> 8) & 0x07), (s & 0xFF)];
console.log("Data to send (score r):", data)
encodeAndSend(new Uint8Array(data), true);
last.scoreRouge = t.scoreState.scoreRouge
}
}, 250)
}
if (last.scoreBleu !== t.scoreState.scoreBleu) {
clearTimeout(last.time_sb)
last.time_sb = setTimeout(() => {
if (last.scoreBleu !== t.scoreState.scoreBleu) {
const b = t.scoreState.scoreBleu < 0
const s = b ? -t.scoreState.scoreBleu : t.scoreState.scoreBleu
const data = [0x60 + 0x10 + (b << 3) + ((s >> 8) & 0x07), (s & 0xFF)];
console.log("Data to send (score b):", data)
encodeAndSend(new Uint8Array(data), true);
last.scoreBleu = t.scoreState.scoreBleu
}
}, 250)
}
}, [state])
useEffect(() => {
const last = lastSend.current;
clearTimeout(last.scoreBleu)
clearTimeout(last.scoreRouge)
clearTimeout(last.time_id)
clearTimeout(last.time_chronoText)
clearInterval(last.time_chronoInter)
last.id = 0
last.scoreBleu = 0
last.scoreRouge = 0
last.chronoState = null
}, [table])
return (
<div>
<input
type="text"
value={table}
onChange={(e) => setTable(e.target.value)}
placeholder="Nom de la zone"
/>
<span>{isReady ? 'Actif' : "Zone non configurée"}</span>
</div>
);
};
export default AudioEncoder;

View File

@@ -8,16 +8,6 @@ const vite_url = import.meta.env.VITE_URL;
const options = {
order: [ 'querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
caches: [],
convertDetectedLanguage: (lng) => {
const supportedLngs = ['en', 'fr'];
const languagePart = lng.split('-')[0];
if (supportedLngs.includes(languagePart)) {
return languagePart; // Manually return 'en' from 'en-US'
}
return lng;
},
}
i18n
@@ -32,12 +22,8 @@ i18n
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: {
'fr-FR': ['fr'],
default: ['en']
},
supportedLngs: ['fr', 'en'],
nonExplicitSupportedLngs: true,
fallbackLng: 'fr',
debug: vite_url.startsWith('http://localhost'),
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@@ -74,7 +74,7 @@ export function OBSProvider({children}) {
}
function getElementName(element) {
return `sub${sessionStorage.getItem("liceName") || 1}.${element}`
return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}`
}
export function useOBS() {

View File

@@ -23,6 +23,8 @@ export function MemberList({source}) {
const {t} = useTranslation();
const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]);
const [showLicenceState, setShowLicenceState] = useState((sessionStorage.getItem("showLicenceState") || "false") === "true");
const setFilter = (filter) => {
navigate("#" + encodeURI(JSON.stringify(filter)))
@@ -35,7 +37,7 @@ export function MemberList({source}) {
payment: 2,
order: "",
categorie: "",
archived: sessionStorage.getItem("showMembreArchived") || true,
archived: false,
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
}
@@ -70,11 +72,23 @@ export function MemberList({source}) {
club: e.club,
categorie: e.categorie,
licence_number: e.licence,
licence: data.additionalData?.find(licence => licence.membre === e.id)
licence: showLicenceState ? licenceData.find(licence => licence.membre === e.id) : null
})
}
setMemberData(data2);
}, [data]);
}, [data, licenceData]);
useEffect(() => {
sessionStorage.setItem("showLicenceState", showLicenceState);
if (!showLicenceState)
return;
toast.promise(
apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
.then(data => {
setLicenceData(data.data);
});
}, [showLicenceState]);
const search = (search) => {
if (search === filter.search)
@@ -88,8 +102,8 @@ export function MemberList({source}) {
<div className="col-lg-9">
<SearchBar search={search} defaultValue={filter.search}/>
{data
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} page={filter.page}
setPage={e => setFilter({...filter, page: e})} source={source}/>
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState}
page={filter.page} setPage={e => setFilter({...filter, page: e})} source={source}/>
: error
? <AxiosError error={error}/>
: <Def/>
@@ -116,11 +130,10 @@ export function MemberList({source}) {
<div className="card mb-4">
<div className="card-header">{t('filtre')}</div>
<div className="card-body">
<FiltreBar showArchived={filter.archived}
setShowArchived={e => {
setFilter({...filter, archived: e})
sessionStorage.setItem("showMembreArchived", e);
}}
<FiltreBar showLicenceState={showLicenceState}
setShowLicenceState={setShowLicenceState}
showArchived={filter.archived}
setShowArchived={e => setFilter({...filter, archived: e})}
clubFilter={filter.club}
setClubFilter={e => setFilter({...filter, club: e})}
source={source}
@@ -150,7 +163,6 @@ export function MemberList({source}) {
function FileOutput() {
const {t} = useTranslation();
function formatColumnDate(worksheet, col) {
const range = XLSX.utils.decode_range(worksheet['!ref'])
// note: range.s.r + 1 skips the header row
@@ -169,7 +181,6 @@ function FileOutput() {
toast.promise(
apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export"))
.then(data => {
console.log(data)
const dataOut = []
for (const e of data.data) {
const tmp = {
@@ -343,7 +354,7 @@ function FileInput() {
);
}
function MakeCentralPanel({data, visibleMember, navigate, page, setPage, source}) {
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) {
const {t} = useTranslation();
const pages = []
@@ -363,7 +374,7 @@ function MakeCentralPanel({data, visibleMember, navigate, page, setPage, source}
})}</small>
<div className="list-group">
{visibleMember.map(member => (
<MakeRow key={member.id} member={member} navigate={navigate} source={source}/>))}
<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))}
</div>
</div>
<div className="mb-4">
@@ -380,11 +391,11 @@ function MakeCentralPanel({data, visibleMember, navigate, page, setPage, source}
</>
}
function MakeRow({member, navigate, source}) {
function MakeRow({member, showLicenceState, navigate, source}) {
const rowContent = <>
<div className="row" style={{padding: "0.6em 0"}}>
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "}
{(member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span>
{(showLicenceState && member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span>
<div className="ms-2 col-auto">
<div className="fw-bold">{member.fname} {member.lname}</div>
</div>
@@ -400,7 +411,7 @@ function MakeRow({member, navigate, source}) {
</>
if (member.licence != null) {
if (showLicenceState && member.licence != null) {
return <a className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-"
+ (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))}
style={{padding: "0 1em"}}
@@ -498,6 +509,8 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
}
function FiltreBar({
showLicenceState,
setShowLicenceState,
showArchived,
setShowArchived,
clubFilter,
@@ -513,6 +526,9 @@ function FiltreBar({
const {t} = useTranslation();
return <div>
<div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label={t('membre.filtre.licence')}/>
</div>
<div className="mb-3">
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/>
</div>

View File

@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, getFirstDateOfSaison, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {useTranslation} from "react-i18next";
@@ -51,34 +51,13 @@ export function LicenceCard({userData}) {
dispatch({type: 'SORT'})
}, [data]);
const handleAsk = () => {
const currentLicence = licences[0];
let certif = undefined;
if (currentLicence != null && currentLicence.certificate != null) {
const strings = currentLicence.certificate.split('¤');
if (currentLicence.saison === getSaison()) {
certif = currentLicence.certificate;
} else if (strings.length > 1) {
const date = new Date(strings[1]);
const max = getFirstDateOfSaison();
max.setFullYear(max.getFullYear() - 2);
if (max < date) {
certif = currentLicence.certificate;
}
}
}
setModal({id: -1, membre: userData.id, certificate: certif});
}
return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid">
<div className="row">
<div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
onClick={handleAsk}>{t('button.ajouter')}
onClick={_ => setModal({id: -1, membre: userData.id})}>{t('button.ajouter')}
</button>
</div>
</div>
@@ -162,6 +141,13 @@ function ModalContent({licence, dispatch}) {
if (licence.id !== -1) {
setNew(false)
setSaison(licence.saison)
if (licence.certificate === null) {
setCertificateBy("")
setCertificateDate("")
} else {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
}
setValidate(licence.validate)
setPay(licence.pay);
} else {
@@ -172,13 +158,6 @@ function ModalContent({licence, dispatch}) {
setValidate(false)
setPay(false);
}
if (licence.certificate) {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
} else {
setCertificateBy("")
setCertificateDate("")
}
}, [licence]);
return <form onSubmit={e => sendLicence(e, dispatch)}>

View File

@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, getFirstDateOfSaison, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {ColoredText} from "../../../components/ColoredCircle.jsx";
import {useTranslation} from "react-i18next";
@@ -32,7 +32,7 @@ function licenceReducer(licences, action) {
}
export function LicenceCard({userData}) {
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: null}
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false}
const {t} = useTranslation();
const setLoading = useLoadingSwitcher()
@@ -48,33 +48,13 @@ export function LicenceCard({userData}) {
dispatch({type: 'SORT'})
}, [data]);
const handleAsk = () => {
const currentLicence = licences[0];
let certif = undefined;
if (currentLicence != null && currentLicence.certificate != null) {
const strings = currentLicence.certificate.split('¤');
if (currentLicence.saison === getSaison()) {
certif = currentLicence.certificate;
} else if (strings.length > 1) {
const date = new Date(strings[1]);
const max = getFirstDateOfSaison();
max.setFullYear(max.getFullYear() - 2);
if (max < date)
certif = currentLicence.certificate;
}
}
setModal({...defaultLicence, certificate: certif});
}
return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid">
<div className="row">
<div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
onClick={handleAsk}
onClick={() => setModal(defaultLicence)}
disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')}
</button>
</div>
@@ -147,14 +127,15 @@ function ModalContent({licence, dispatch}) {
useEffect(() => {
if (licence.id !== -1) {
setNew(false)
if (licence.certificate === null) {
setCertificateBy("")
setCertificateDate("")
} else {
setNew(true)
}
if (licence.certificate) {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
}
} else {
setNew(true)
setCertificateBy("")
setCertificateDate("")
}

View File

@@ -331,8 +331,9 @@ function Menu({menuActions, compUuid}) {
<strong>{t('config.obs.warn1')}</strong>
<div className="input-group mb-3">
<span className="input-group-text">{t('adresseDuServeur')}</span>
<input type="text" className="form-control" placeholder="ws://127.0.0.1:4455/" aria-label=""
defaultValue={"ws://127.0.0.1:4455/"}/>
<span className="input-group-text">{t('config.obs.ws')}</span>
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
defaultValue={"127.0.0.1:4455"}/>
<span className="input-group-text">/</span>
</div>
<div className="input-group mb-3">
@@ -406,7 +407,6 @@ function PrintModal({menuActions}) {
const [allCatEmpty, setAllCatEmpty] = useState(false);
const [podium, setPodium] = useState(false);
const [podiumRank, setPodiumRank] = useState(4);
const [podiumClub, setPodiumClub] = useState(false);
const [presetSelect, setPresetSelect] = useState(-1)
@@ -416,20 +416,12 @@ function PrintModal({menuActions}) {
const podiumPromise = (podiumRank_) => {
return sendRequest("getPodium", {}).then(data => {
return [welcomeData?.name + " - " + t('podium'), [
return [welcomeData?.name + " - " + "Podium", [
{type: "podium", params: ({data, maxRank: podiumRank_, minRank: Math.min(4, podiumRank_)})},
]];
});
}
const podiumClubPromise = () => {
return sendRequest("getPodiumClub", {}).then(data => {
return [welcomeData?.name + " - " + t('classementDesClub', {ns: "result"}), [
{type: "podiumClub", params: ({data})},
]];
});
}
const print = (action) => {
const pagesPromise = [];
@@ -445,9 +437,6 @@ function PrintModal({menuActions}) {
if (podium)
pagesPromise.push(podiumPromise(podiumRank));
if (podiumClub)
pagesPromise.push(podiumClubPromise());
toast.promise(
toDataURL("/Logo-FFSAF-2023.png").then(logo => {
return Promise.allSettled(pagesPromise).then(results => {
@@ -524,7 +513,7 @@ function PrintModal({menuActions}) {
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={podium} id="checkPrint7"
onChange={e => setPodium(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint7">{t('podium')}</label>
<label className="form-check-label" htmlFor="checkPrint7">Podium</label>
</div>
{podium &&
<div style={{marginLeft: "1em"}}>
@@ -532,12 +521,6 @@ function PrintModal({menuActions}) {
<input type="range" className="form-range" min="1" max="20" step="1" id="range3" value={podiumRank}
onChange={e => setPodiumRank(Number(e.target.value))}/>
</div>}
<div className="form-check">
<input className="form-check-input" type="checkbox" checked={podiumClub} id="checkPrint8"
onChange={e => setPodiumClub(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkPrint8">{t('classementDesClub', {ns: "result"})}</label>
</div>
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("show")}>{t('afficher')}</button>

View File

@@ -41,7 +41,6 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS();
const publicAffDispatch = usePubAffDispatch();
const {connected, setText} = useOBS();
const {t} = useTranslation("cm");
@@ -71,8 +70,6 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
useEffect(() => {
setText("poule", cat ? cat.name : "");
if (!cat)
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
}, [cat, connected]);
return <>

View File

@@ -48,7 +48,7 @@ export function PointPanel({menuActions}) {
<div className="col row align-items-center">
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
{ /*<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button> */}
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
</div>
<SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/>
</div>

View File

@@ -160,7 +160,7 @@ function Menu({menuActions}) {
} else {
importOBSConfiguration()
.then(config => {
connect(config.adresse, config.password, config.assets_dir);
connect("ws://" + config.adresse + "/", config.password, config.assets_dir);
})
.catch(() => {
toast.error(t('aucuneConfigurationObs'));

View File

@@ -568,9 +568,7 @@ function MatchList({matches, cat, groups, reducer, classement = false}) {
const {active, over} = event;
if (active.id !== over.id) {
let newIndex = marches2.findIndex(m => m.id === over.id);
if (newIndex > 0)
newIndex = marches2[newIndex].categorie_ord;
const newIndex = marches2.findIndex(m => m.id === over.id);
reducer({type: 'REORDER', payload: {id: active.id, pos: newIndex}});
sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => {
})

View File

@@ -11,7 +11,6 @@ import {AxiosError} from "../../../components/AxiosError.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {useTranslation} from "react-i18next";
import {CardsProvider} from "../../../hooks/useCard.jsx";
import AudioEncoder from "../../../components/cm/AudioEncoder.jsx";
const vite_url = import.meta.env.VITE_URL;
@@ -74,7 +73,6 @@ function HomeComp() {
<Route path="/" element={<Home2 perm={perm}/>}/>
<Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/>
<Route path="/table" element={<CMTable/>}/>
<Route path="/view/audio" element={<AudioEncoder/>}/>
</Routes>
</LoadingProvider>
</CardsProvider>

View File

@@ -19,7 +19,7 @@ function reducer(state, action) {
}
}
export function useTablesState() {
export function StateWindow({document}) {
const {sendRequest, dispatch} = useWS();
const [state, dispatchState] = useReducer(reducer, [])
@@ -58,12 +58,6 @@ export function useTablesState() {
}
}, [])
return {state};
}
export function StateWindow({document}) {
const {state} = useTablesState();
document.title = "État des tables de marque";
document.body.className = "overflow-hidden";
@@ -72,7 +66,7 @@ export function StateWindow({document}) {
<div className="d-flex flex-row flex-wrap justify-content-around align-items-center align-content-around h-100 p-2 overflow-auto">
{state.sort((a, b) => a.liceName.localeCompare(b.liceName)).map((table, index) =>
<div key={index} className="card d-inline-flex flex-grow-1 align-self-stretch" style={{minWidth: "25em", maxWidth: "30em"}}>
<ShowState table={table}/>
<ShowState table={table} dispatch={dispatchState}/>
</div>)
}
</div>
@@ -197,7 +191,7 @@ function PrintChrono({chrono}) {
const timer = setInterval(() => {
let currentDuration = chrono.configTime
if (chrono.state === 2) {
currentDuration = chrono.configPause
currentDuration = (chrono.state === 0) ? 10000 : chrono.configPause
}
const timeStr = (chrono.state === 1 ? " Match - " : " Pause - ") + timePrint(currentDuration - getTime()) + (isRunning() ? "" : " (arrêté)")

View File

@@ -39,8 +39,7 @@ export function ResultView() {
{resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/>
|| resultShow && resultShow === "club" && <ClubList uuid={uuid}/>
|| resultShow && resultShow === "comb" && <CombList uuid={uuid}/>
|| resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>
|| resultShow && resultShow === "clubs" && <ClubsResult uuid={uuid}/>}
|| resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>}
</div>
</div>
</>
@@ -67,10 +66,6 @@ function MenuBar({resultShow, setResultShow}) {
<a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a>
</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>
/*
@@ -455,43 +450,6 @@ function CombResult({uuid, combId}) {
</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('classementDesClub')} :</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}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1)

View File

@@ -56,10 +56,8 @@ export function MarchReducer(datas, action) {
return datas.sort(action.payload)
case 'REORDER':
const oldIndex = datas.findIndex(data => data.id === action.payload.id)
if (oldIndex === -1)
if (oldIndex === -1 || datas[oldIndex].categorie_ord === action.payload.pos)
return datas // Do nothing
if (datas[oldIndex].categorie_ord === action.payload.pos)
return [...datas] // Do nothing
const oldPos = datas[oldIndex].categorie_ord
const newPos = action.payload.pos

View File

@@ -117,14 +117,6 @@ export function getSaison(currentDate = new Date()) {
}
}
export function getFirstDateOfSaison() {
const year = getSaison();
const firstDate = new Date(year, 8, 1); // 8 = septembre, 1 = premier jour
firstDate.setHours(0, 0, 0, 0); // Heure, minutes, secondes, millisecondes à 0
return firstDate;
}
export function getCatName(cat) {
switch (cat) {
case "SUPER_MINI":

View File

@@ -45,9 +45,6 @@ export function makePDF(action, pagesList, name, c_name, getComb, t, logo) {
case "podium":
generatePodium(context);
break;
case "podiumClub":
generateClubPodium(context);
break;
default:
break
}
@@ -375,7 +372,7 @@ function generateCategoriePDF({pdf_doc, cat, matches, groups, getComb, cards_v,
}
function generatePodium({pdf_doc, data, t, logo, c_name, minRank = 4, maxRank = 4}) {
makeHeader(pdf_doc, c_name, t('podium'), logo)
makeHeader(pdf_doc, c_name, "Podium", logo)
const data2 = data.sort((a, b) => {
let tmp = sortCategories(a.categorie, b.categorie);
@@ -444,59 +441,3 @@ function generatePodium({pdf_doc, data, t, logo, c_name, minRank = 4, maxRank =
}
pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY);
}
function generateClubPodium({pdf_doc, data, t, logo, c_name}) {
makeHeader(pdf_doc, c_name, t('podiumDesClubs'), logo)
const body = data.map(c => [
{content: [c.name], styles: {halign: "center"}},
{content: c.score[0], styles: {halign: "center"}},
{content: c.score[1], styles: {halign: "center"}},
{content: c.score[2], styles: {halign: "center"}},
{content: c.tt_score, styles: {halign: "center"}},
]);
let rank = 0;
let lastScores = null;
let lastB = null;
for (const b of body) {
const scores = [b[1].content, b[2].content, b[3].content, b[4].content];
if (lastScores !== scores) {
rank++;
b.unshift({content: rank, styles: {halign: "center"}})
lastB = b
} else {
lastB[1].content.push(b[0].content);
delete body.indexOf(b);
}
}
for (const b of body) {
b[1].content = b[1].content.join(", ")
}
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
columnStyles: {
0: {cellWidth: 35},
1: {cellWidth: "auto"},
2: {cellWidth: 45},
3: {cellWidth: 45},
4: {cellWidth: 45},
5: {cellWidth: 40},
},
pageBreak: "avoid",
showHead: 'firstPage',
head: [[
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
{content: t('club', {ns: 'result'}), styles: {halign: "center"}},
{content: t('1er', {ns: 'result'}), styles: {halign: "center"}},
{content: t('2eme', {ns: 'result'}), styles: {halign: "center"}},
{content: t('3eme', {ns: 'result'}), styles: {halign: "center"}},
{content: t('scores', {ns: 'result'}), styles: {halign: "center"}},
]],
body: body,
rowPageBreak: 'auto',
theme: 'grid',
})
}