Compare commits

..

5 Commits

Author SHA1 Message Date
e3a1d1c50b Merge pull request 'dev' (#94) from dev into master
Reviewed-on: #94
2026-01-03 20:50:18 +00:00
3933954e09 feat: add permission setting to admin page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m44s
2026-01-03 21:49:36 +01:00
959e356fb9 fix: quick add auto complet 2026-01-03 11:22:04 +01:00
3b5955ff31 fix: rm match with cardboard 2026-01-02 23:44:53 +01:00
e8ee5811a0 feat: avoid ws reconnect when user logged out 2026-01-02 23:12:22 +01:00
14 changed files with 246 additions and 53 deletions

View File

@ -67,8 +67,15 @@ public class CompetitionModel {
@Column(name = "table_") @Column(name = "table_")
List<String> table = new ArrayList<>(); List<String> table = new ArrayList<>();
@Column(columnDefinition = "TEXT")
String data1; String data1;
@Column(columnDefinition = "TEXT")
String data2; String data2;
@Column(columnDefinition = "TEXT")
String data3; String data3;
@Column(columnDefinition = "TEXT")
String data4; String data4;
@Column(columnDefinition = "TEXT")
String config;
} }

View File

@ -98,7 +98,13 @@ public class CompetitionService {
Cache cacheNoneAccess; Cache cacheNoneAccess;
public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) { public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) {
return permService.hasViewPerm(securityCtx, id).map(CompetitionData::fromModelLight); return permService.hasViewPerm(securityCtx, id).map(cm -> {
CompetitionData out = CompetitionData.fromModelLight(cm);
if (cm.getAdmin() != null) {
out.setCanEditRegisters(cm.getAdmin().stream().anyMatch(u -> u.equals(securityCtx.getSubject())));
}
return out;
});
} }
public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) { public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
@ -106,7 +112,8 @@ public class CompetitionService {
return Uni.createFrom() return Uni.createFrom()
.item(new CompetitionData(null, "", "", "", "", new Date(), new Date(), .item(new CompetitionData(null, "", "", "", "", new Date(), new Date(),
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true, CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
null, "", "", null, true, "", "", "", "")); null, "", "", null, true, true,
"", "", "", "", "{}"));
} }
return permService.hasAdminViewPerm(securityCtx, id) return permService.hasAdminViewPerm(securityCtx, id)
.chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc()) .chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
@ -129,6 +136,14 @@ public class CompetitionService {
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList()); out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
out.forEach(competition -> competition.setCanEdit(true)); out.forEach(competition -> competition.setCanEdit(true));
})) }))
.chain(ids -> repository.list("id NOT IN ?1 AND ?2 IN admin", ids, securityCtx.getSubject())
.map(cm -> {
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
out.forEach(competition -> competition.setCanEditRegisters(true));
List<Long> ids2 = new ArrayList<>(ids);
ids2.addAll(cm.stream().map(CompetitionModel::getId).toList());
return ids2;
}))
.call(ids -> .call(ids ->
repository.list("id NOT IN ?1 AND (publicVisible = TRUE OR registerMode IN ?2)", ids, repository.list("id NOT IN ?1 AND (publicVisible = TRUE OR registerMode IN ?2)", ids,
securityCtx.isClubAdmin() ? List.of(RegisterMode.FREE, RegisterMode.HELLOASSO, securityCtx.isClubAdmin() ? List.of(RegisterMode.FREE, RegisterMode.HELLOASSO,
@ -165,8 +180,9 @@ public class CompetitionService {
public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx, public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx,
CompetitionSystem system) { CompetitionSystem system) {
return repository.list("system = ?1", system) return repository.list("system = ?1", system)
.chain(l -> Uni.join().all(l.stream().map(cm -> permService.hasTablePerm(securityCtx, cm)).toList()) .chain(l -> Uni.join().all(l.stream().map(cm ->
.andCollectFailures()) permService.hasTablePerm(securityCtx, cm).onFailure().recoverWithNull()
).toList()).andCollectFailures())
.map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList()); .map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList());
} }
@ -513,6 +529,64 @@ public class CompetitionService {
.call(__ -> cache.invalidate(id)); .call(__ -> cache.invalidate(id));
} }
public Uni<SimpleCompetData> getInternalData(SecurityCtx securityCtx, Long id) {
return permService.hasEditPerm(securityCtx, id)
.invoke(Unchecked.consumer(cm -> {
if (cm.getSystem() != CompetitionSystem.INTERNAL)
throw new DBadRequestException("Competition is not INTERNAL");
}))
.chain(competitionModel -> {
SimpleCompetData data = SimpleCompetData.fromModel(competitionModel);
return vertx.getOrCreateContext().executeBlocking(() -> {
if (competitionModel.getAdmin() != null)
data.setAdmin(
competitionModel.getAdmin().stream().map(uuid -> keycloakService.getUserById(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
if (competitionModel.getTable() != null)
data.setTable(
competitionModel.getTable().stream().map(uuid -> keycloakService.getUserById(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
return data;
});
});
}
public Uni<Void> setInternalData(SecurityCtx securityCtx, SimpleCompetData data) {
return permService.hasEditPerm(securityCtx, data.getId())
.invoke(Unchecked.consumer(cm -> {
if (cm.getSystem() != CompetitionSystem.INTERNAL)
throw new DBadRequestException("Competition is not INTERNAL");
}))
.chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> {
ArrayList<String> admin = new ArrayList<>();
ArrayList<String> table = new ArrayList<>();
for (String username : data.getAdmin()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException("User " + username + " not found");
admin.add(opt.get().getId());
}
for (String username : data.getTable()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException("User " + username + " not found");
table.add(opt.get().getId());
}
cm.setAdmin(admin);
cm.setTable(table);
cm.setConfig(data.getConfigForInternal());
return cm;
}))
.chain(cm -> Panache.withTransaction(() -> repository.persist(cm)))
.replaceWithVoid();
}
public Uni<SimpleCompetData> getSafcaData(SecurityCtx securityCtx, Long id) { public Uni<SimpleCompetData> getSafcaData(SecurityCtx securityCtx, Long id) {
return permService.getSafcaConfig(id) return permService.getSafcaConfig(id)
.call(Unchecked.function(o -> { .call(Unchecked.function(o -> {

View File

@ -377,7 +377,11 @@ public class KeycloakService {
public Optional<UserRepresentation> getUser(UUID userId) { public Optional<UserRepresentation> getUser(UUID userId) {
UserResource user = keycloak.realm(realm).users().get(userId.toString()); return getUserById(userId.toString());
}
public Optional<UserRepresentation> getUserById(String userId) {
UserResource user = keycloak.realm(realm).users().get(userId);
if (user == null) if (user == null)
return Optional.empty(); return Optional.empty();
else else

View File

@ -71,6 +71,14 @@ public class CompetitionEndpoints {
return service.getSafcaData(securityCtx, id); return service.getSafcaData(securityCtx, id);
} }
@GET
@Path("{id}/internalData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleCompetData> getInternalData(@PathParam("id") Long id) {
return service.getInternalData(securityCtx, id);
}
@GET @GET
@Path("all") @Path("all")
@ -95,6 +103,14 @@ public class CompetitionEndpoints {
return service.setSafcaData(securityCtx, data); return service.setSafcaData(securityCtx, data);
} }
@POST
@Path("/internalData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> setInternalData(SimpleCompetData data) {
return service.setInternalData(securityCtx, data);
}
@DELETE @DELETE
@Path("{id}") @Path("{id}")
@Authenticated @Authenticated

View File

@ -35,10 +35,12 @@ public class CompetitionData {
private String owner; private String owner;
private List<SimpleRegister> registers; private List<SimpleRegister> registers;
private boolean canEdit; private boolean canEdit;
private boolean canEditRegisters;
private String data1; private String data1;
private String data2; private String data2;
private String data3; private String data3;
private String data4; private String data4;
private String config;
public static CompetitionData fromModel(CompetitionModel model) { public static CompetitionData fromModel(CompetitionModel model) {
if (model == null) if (model == null)
@ -47,8 +49,8 @@ public class CompetitionData {
return new CompetitionData(model.getId(), model.getName(), model.getDescription(), model.getAdresse(), return new CompetitionData(model.getId(), model.getName(), model.getDescription(), model.getAdresse(),
model.getUuid(), model.getDate(), model.getTodate(), model.getSystem(), model.getUuid(), model.getDate(), model.getTodate(), model.getSystem(),
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(), model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
model.getClub().getId(), model.getClub().getName(), model.getOwner(), null, false, model.getClub().getId(), model.getClub().getName(), model.getOwner(), null, false, false,
model.getData1(), model.getData2(), model.getData3(), model.getData4()); model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig());
} }
public static CompetitionData fromModelLight(CompetitionModel model) { public static CompetitionData fromModelLight(CompetitionModel model) {
@ -58,14 +60,15 @@ public class CompetitionData {
CompetitionData out = new CompetitionData(model.getId(), model.getName(), model.getDescription(), CompetitionData out = new CompetitionData(model.getId(), model.getName(), model.getDescription(),
model.getAdresse(), "", model.getDate(), model.getTodate(), null, model.getAdresse(), "", model.getDate(), model.getTodate(), null,
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(), model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
null, model.getClub().getName(), "", null, false, null, model.getClub().getName(), "", null, false, false,
"", "", "", ""); "", "", "", "", "{}");
if (model.getRegisterMode() == RegisterMode.HELLOASSO) { if (model.getRegisterMode() == RegisterMode.HELLOASSO) {
out.setData1(model.getData1()); out.setData1(model.getData1());
out.setData2(model.getData2()); out.setData2(model.getData2());
} }
return out; return out;
} }

View File

@ -1,5 +1,9 @@
package fr.titionfire.ffsaf.rest.data; package fr.titionfire.ffsaf.rest.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import fr.titionfire.ffsaf.net2.data.SimpleCompet; import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -25,4 +29,41 @@ public class SimpleCompetData {
return new SimpleCompetData(compet.id(), compet.show_blason(), compet.show_flag(), return new SimpleCompetData(compet.id(), compet.show_blason(), compet.show_flag(),
new ArrayList<>(), new ArrayList<>()); new ArrayList<>(), new ArrayList<>());
} }
public static SimpleCompetData fromModel(CompetitionModel competitionModel) {
if (competitionModel == null)
return null;
boolean show_blason = true;
boolean show_flag = false;
if (competitionModel.getConfig() != null) {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(competitionModel.getConfig());
if (rootNode.has("show_blason"))
show_blason = rootNode.get("show_blason").asBoolean();
if (rootNode.has("show_flag"))
show_flag = rootNode.get("show_flag").asBoolean();
} catch (JsonProcessingException ignored) {
}
}
return new SimpleCompetData(competitionModel.getId(), show_blason, show_flag,
new ArrayList<>(), new ArrayList<>());
}
public String getConfigForInternal(){
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.createObjectNode()
.put("show_blason", this.show_blason)
.put("show_flag", this.show_flag);
try {
return objectMapper.writeValueAsString(rootNode);
} catch (JsonProcessingException e) {
return "{}";
}
}
} }

View File

@ -108,8 +108,8 @@ public class CompetitionWS {
.call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> PermLevel.ADMIN) .call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> PermLevel.ADMIN)
.onFailure() .onFailure()
.recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> PermLevel.TABLE)) .recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> PermLevel.TABLE))
.onFailure() //.onFailure()
.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW)) //.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW))
.invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem.toString())) .invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem.toString()))
.invoke(prem -> LOGGER.infof("Connection permission: %s", prem)) .invoke(prem -> LOGGER.infof("Connection permission: %s", prem))
.onFailure().transform(t -> new ForbiddenException())) .onFailure().transform(t -> new ForbiddenException()))

View File

@ -3,10 +3,7 @@ package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CategoryModel; import fr.titionfire.ffsaf.data.model.CategoryModel;
import fr.titionfire.ffsaf.data.model.MatchModel; import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.model.TreeModel; import fr.titionfire.ffsaf.data.model.TreeModel;
import fr.titionfire.ffsaf.data.repository.CategoryRepository; import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.data.repository.TreeRepository;
import fr.titionfire.ffsaf.domain.entity.MatchEntity; import fr.titionfire.ffsaf.domain.entity.MatchEntity;
import fr.titionfire.ffsaf.domain.entity.TreeEntity; import fr.titionfire.ffsaf.domain.entity.TreeEntity;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
@ -46,6 +43,9 @@ public class RCategorie {
@Inject @Inject
TreeRepository treeRepository; TreeRepository treeRepository;
@Inject
CardboardRepository cardboardRepository;
private Uni<CategoryModel> getById(long id, WebSocketConnection connection) { private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
return categoryRepository.findById(id) return categoryRepository.findById(id)
.invoke(Unchecked.consumer(o -> { .invoke(Unchecked.consumer(o -> {
@ -210,6 +210,7 @@ public class RCategorie {
public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) { public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) {
return getById(id, connection) return getById(id, connection)
.call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId()) .call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId())
.call(__ -> cardboardRepository.delete("match.category = ?1", cat))
.call(__ -> matchRepository.delete("category = ?1", cat)))) .call(__ -> matchRepository.delete("category = ?1", cat))))
.chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat))) .chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat)))
.invoke(__ -> SSCategorie.sendDelCategory(connection, id)) .invoke(__ -> SSCategorie.sendDelCategory(connection, id))

View File

@ -44,6 +44,9 @@ public class RMatch {
@Inject @Inject
CompetitionGuestRepository competitionGuestRepository; CompetitionGuestRepository competitionGuestRepository;
@Inject
CardboardRepository cardboardRepository;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) { private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id) return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> { .invoke(Unchecked.consumer(o -> {
@ -277,7 +280,10 @@ public class RMatch {
@WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN) @WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN)
public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) { public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
return getById(idMatch, connection) return getById(idMatch, connection)
.chain(matchModel -> Panache.withTransaction(() -> matchRepository.delete(matchModel))) .map(__ -> idMatch)
.chain(l -> Panache.withTransaction(() ->
cardboardRepository.delete("match.id = ?1", l)
.chain(__ -> matchRepository.delete("id = ?1", l))))
.invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch)) .invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
.replaceWithVoid(); .replaceWithVoid();
} }

View File

@ -1,6 +1,4 @@
import {createContext, useContext, useEffect, useId, useReducer, useRef, useState} from "react"; import {createContext, useContext, useEffect, useId, useReducer, useRef, useState} from "react";
import {apiAxios} from "../utils/Tools.js";
import {toast} from "react-toastify";
import {useAuth} from "./useAuth.jsx"; import {useAuth} from "./useAuth.jsx";
function uuidv4() { function uuidv4() {
@ -45,6 +43,7 @@ export function WSProvider({url, onmessage, children}) {
const id = useId(); const id = useId();
const {is_authenticated} = useAuth() const {is_authenticated} = useAuth()
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
const [doReconnect, setDoReconnect] = useState(false)
const [state, dispatch] = useReducer(reducer, {listener: []}) const [state, dispatch] = useReducer(reducer, {listener: []})
const ws = useRef(null) const ws = useRef(null)
const listenersRef = useRef([]) const listenersRef = useRef([])
@ -59,10 +58,33 @@ export function WSProvider({url, onmessage, children}) {
listenersRef.current = state.listener listenersRef.current = state.listener
}, [state.listener]) }, [state.listener])
useEffect(() => {
if (!doReconnect && !is_authenticated && isReady)
return;
const timer = setInterval(() => {
if (isReady || !doReconnect || !is_authenticated)
return;
console.log("WSProvider: reconnecting to", url);
try {
const newSocket = new WebSocket(url)
newSocket.onopen = ws.current.onopen
newSocket.onclose = ws.current.onclose
newSocket.onmessage = ws.current.onmessage
ws.current = newSocket
}catch (e) {
}
}, 5000);
return () => clearInterval(timer);
}, [isReady, doReconnect, is_authenticated]);
useEffect(() => { useEffect(() => {
if (!mountCounter[id]) if (!mountCounter[id])
mountCounter[id] = 0 mountCounter[id] = 0
mountCounter[id] += 1 mountCounter[id] += 1
setDoReconnect(true)
console.log(`WSProvider ${id} mounted ${mountCounter[id]} time(s)`); console.log(`WSProvider ${id} mounted ${mountCounter[id]} time(s)`);
if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)){ if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)){
@ -72,24 +94,6 @@ export function WSProvider({url, onmessage, children}) {
socket.onopen = () => setIsReady(true) socket.onopen = () => setIsReady(true)
socket.onclose = () => { socket.onclose = () => {
setIsReady(false) setIsReady(false)
if (mountCounter[id] > 0) {
setTimeout(() => {
//if (is_authenticated){
console.log("WSProvider: reconnecting to", url);
try {
const newSocket = new WebSocket(url)
ws.current = newSocket
newSocket.onopen = socket.onopen
newSocket.onclose = socket.onclose
newSocket.onmessage = socket.onmessage
}catch (e) {
}
//}else{
// console.log("WSProvider: not reconnecting, user is not authenticated");
//}
}, 5000)
}
} }
socket.onmessage = (event) => { socket.onmessage = (event) => {
const msg = JSON.parse(event.data) const msg = JSON.parse(event.data)
@ -132,6 +136,7 @@ export function WSProvider({url, onmessage, children}) {
setTimeout(() => { setTimeout(() => {
console.log(`WSProvider ${id} checking for close, ${mountCounter[id]} instance(s) remain`); console.log(`WSProvider ${id} checking for close, ${mountCounter[id]} instance(s) remain`);
if (mountCounter[id] === 0) { if (mountCounter[id] === 0) {
setDoReconnect(false)
console.log("WSProvider: closing connection to", url); console.log("WSProvider: closing connection to", url);
ws.current.close() ws.current.close()
} }
@ -139,13 +144,14 @@ export function WSProvider({url, onmessage, children}) {
} }
}, []) }, [])
const send = (uuid, code, type, data, resolve = () => { const send2 = (uuid, code, type, data, resolve = () => {
}, reject = () => { }, reject = () => {
}) => { }) => {
if (!isReadyRef.current) { if (!isReadyRef.current) {
reject("WebSocket is not connected"); reject("WebSocket is not connected");
return; return;
} }
if (type === "REQUEST") { if (type === "REQUEST") {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
reject("timeout"); reject("timeout");
@ -172,6 +178,26 @@ export function WSProvider({url, onmessage, children}) {
data: data data: data
})) }))
} }
const send = (uuid, code, type, data, resolve = () => {
}, reject = () => {
}) => {
if (isReadyRef.current) {
send2(uuid, code, type, data, resolve, reject);
}else {
let counter = 0;
const waitInterval = setInterval(() => {
if (isReadyRef.current) {
clearInterval(waitInterval);
send2(uuid, code, type, data, resolve, reject);
}
counter += 1;
if (counter >= 300) { // 30 seconds timeout
clearInterval(waitInterval);
reject("WebSocket is not connected");
}
}, 100);
}
}
const ret = {isReady, dispatch, send, wait_length: callbackRef} const ret = {isReady, dispatch, send, wait_length: callbackRef}

View File

@ -51,9 +51,11 @@ export function CompetitionEdit() {
<Content data={data} refresh={refresh}/> <Content data={data} refresh={refresh}/>
{data.id !== null && <button style={{marginBottom: "1.5em", width: "100%"}} className="btn btn-primary" {data.id !== null && <button style={{marginBottom: "1.5em", width: "100%"}} className="btn btn-primary"
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier les participants</button>} onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier
les participants</button>}
{data.id !== null && data.system === "SAFCA" && <ContentSAFCA data2={data}/>} {data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") &&
<ContentSAFCAAndInternal data2={data} type={data.system}/>}
{data.id !== null && <> {data.id !== null && <>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}> <div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
@ -72,9 +74,12 @@ export function CompetitionEdit() {
</> </>
} }
function ContentSAFCA({data2}) { function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
const getDataPath = type === "SAFCA" ? `/competition/${data2.id}/safcaData` : `/competition/${data2.id}/internalData`
const setDataPath = type === "SAFCA" ? "/competition/safcaData" : "/competition/internalData"
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/competition/${data2.id}/safcaData`, setLoading, 1) const {data, error} = useFetch(getDataPath, setLoading, 1)
const [state, dispatch] = useReducer(SimpleReducer, []) const [state, dispatch] = useReducer(SimpleReducer, [])
const [state2, dispatch2] = useReducer(SimpleReducer, []) const [state2, dispatch2] = useReducer(SimpleReducer, [])
@ -109,7 +114,7 @@ function ContentSAFCA({data2}) {
out['table'] = state2.map(d => d.data) out['table'] = state2.map(d => d.data)
toast.promise( toast.promise(
apiAxios.post(`/competition/safcaData`, out), apiAxios.post(setDataPath, out),
{ {
pending: "Enregistrement des paramètres en cours", pending: "Enregistrement des paramètres en cours",
success: "Paramètres enregistrée avec succès 🎉", success: "Paramètres enregistrée avec succès 🎉",
@ -402,7 +407,8 @@ function Content({data}) {
<li><strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à <li><strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à
chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour
qu'il redirige vers "https://intra.ffsaf.fr/api/webhook/ha". Pour ce faire, depuis la page d'accueil de qu'il redirige vers "https://intra.ffsaf.fr/api/webhook/ha". Pour ce faire, depuis la page d'accueil de
votre association sur HelloAsso, allez dans <strong>Mon compte</strong> &gt; <strong>Paramètres</strong> &gt; votre association sur HelloAsso, allez dans <strong>Mon compte</strong> &gt;
<strong>Paramètres</strong> &gt;
<strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong> <strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong>
dans le champ <strong>Mon URL de callback</strong> et enregister. dans le champ <strong>Mon URL de callback</strong> et enregister.
<img src="/img/HA-help-4.png" alt="" className="img-fluid" style={{objectFit: "contain"}}/></li> <img src="/img/HA-help-4.png" alt="" className="img-fluid" style={{objectFit: "contain"}}/></li>

View File

@ -38,7 +38,7 @@ export function CompetitionRegisterAdmin({source}) {
}, [data, clubFilter, catFilter]); }, [data, clubFilter, catFilter]);
const sendRegister = (new_state) => { const sendRegister = (new_state) => {
toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), { return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: { pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: {
render({data}) { render({data}) {
return data.response.data || "Combattant non trouvé" return data.response.data || "Combattant non trouvé"
@ -153,9 +153,9 @@ function SearchMember({sendRegister}) {
} }
sendRegister({ sendRegister({
licence: member.licence.trim(), licence: member.licence,
fname: member.fname.trim(), fname: member.fname,
lname: member.lname.trim(), lname: member.lname,
weight: "", weight: "",
overCategory: 0, overCategory: 0,
lockEdit: false, lockEdit: false,
@ -169,7 +169,7 @@ function SearchMember({sendRegister}) {
const names = data.map(member => `${member.fname} ${member.lname}`.trim()); const names = data.map(member => `${member.fname} ${member.lname}`.trim());
names.sort((a, b) => a.localeCompare(b)); names.sort((a, b) => a.localeCompare(b));
setSuggestions(names); setSuggestions(names);
}, []); }, [data]);
return <> return <>
{data ? <div className="row mb-3" style={{marginTop: "0.5em"}}> {data ? <div className="row mb-3" style={{marginTop: "0.5em"}}>
@ -335,9 +335,9 @@ function Modal({sendRegister, modalState, setModalState, source}) {
<form onSubmit={e => { <form onSubmit={e => {
e.preventDefault() e.preventDefault()
const new_state = { const new_state = {
licence: licence, licence: Number.isInteger(licence) ? licence : licence.trim(),
fname: fname, fname: fname.trim(),
lname: lname, lname: lname.trim(),
weight: weight, weight: weight,
overCategory: cat, overCategory: cat,
lockEdit: lockEdit, lockEdit: lockEdit,
@ -350,8 +350,10 @@ function Modal({sendRegister, modalState, setModalState, source}) {
new_state.country = country_ new_state.country = country_
new_state.genre = genre new_state.genre = genre
} }
setModalState(new_state)
sendRegister(new_state) sendRegister(new_state)
.then(() => {
setModalState(new_state)
})
}}> }}>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" <h1 className="modal-title fs-5"

View File

@ -79,6 +79,13 @@ function MakeContent({data}) {
href={`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`} target="_blank" href={`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`} target="_blank"
rel="noopener noreferrer">{`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`}</a></p> rel="noopener noreferrer">{`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`}</a></p>
} }
{data.canEditRegisters &&
<div style={{marginTop: "0.5em"}}>
<button type="button" className="btn btn-primary"
onClick={_ => navigate("/competition/" + data.id + "/register")}>Inscription - mode administrateur
</button>
</div>
}
</div> </div>
</div> </div>
} }

View File

@ -342,7 +342,7 @@ function CategoryHeader({cat, setCatId}) {
}, [cats]); }, [cats]);
useEffect(() => { useEffect(() => {
if (cats && cats.length > 0 && !cat || (cats && !cats.find(c => c.id === cat.id))) { if (cats && cats.length > 0 && (!cat || (cats && !cats.find(c => c.id === cat.id)))) {
setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id); setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id);
} else if (cats && cats.length === 0) { } else if (cats && cats.length === 0) {
setModal({}); setModal({});