feat: masse licence validation
This commit is contained in:
parent
7bd5e7baa5
commit
09f6cd7463
@ -20,6 +20,7 @@ import org.hibernate.reactive.mutiny.Mutiny;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
@WithSession
|
@WithSession
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
@ -34,6 +35,9 @@ public class LicenceService {
|
|||||||
@Inject
|
@Inject
|
||||||
SequenceRepository sequenceRepository;
|
SequenceRepository sequenceRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeycloakService keycloakService;
|
||||||
|
|
||||||
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
|
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
|
||||||
return combRepository.findById(id).invoke(checkPerm)
|
return combRepository.findById(id).invoke(checkPerm)
|
||||||
.chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
.chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
|
||||||
@ -48,6 +52,23 @@ public class LicenceService {
|
|||||||
.chain(membres -> repository.find("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres).list());
|
.chain(membres -> repository.find("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres).list());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Uni<?> valideLicences(List<Long> ids) {
|
||||||
|
Uni<String> uni = Uni.createFrom().nullItem();
|
||||||
|
|
||||||
|
for (Long id : ids) {
|
||||||
|
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
|
||||||
|
.chain(model -> {
|
||||||
|
model.setValidate(true);
|
||||||
|
return Panache.withTransaction(() -> repository.persist(model)
|
||||||
|
.call(m -> Mutiny.fetch(m.getMembre())
|
||||||
|
.call(genLicenceNumberAndAccountIfNeed())
|
||||||
|
));
|
||||||
|
}))
|
||||||
|
.map(__ -> "OK");
|
||||||
|
}
|
||||||
|
return uni;
|
||||||
|
}
|
||||||
|
|
||||||
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
||||||
if (form.getId() == -1) {
|
if (form.getId() == -1) {
|
||||||
return combRepository.findById(id).chain(membreModel -> {
|
return combRepository.findById(id).chain(membreModel -> {
|
||||||
@ -58,10 +79,8 @@ public class LicenceService {
|
|||||||
model.setCertificate(form.getCertificate());
|
model.setCertificate(form.getCertificate());
|
||||||
model.setValidate(form.isValidate());
|
model.setValidate(form.isValidate());
|
||||||
return Panache.withTransaction(() -> repository.persist(model)
|
return Panache.withTransaction(() -> repository.persist(model)
|
||||||
.call(m -> (m.isValidate() && membreModel.getLicence() <= 0) ?
|
.call(m -> m.isValidate() ? Uni.createFrom().item(membreModel)
|
||||||
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
.call(genLicenceNumberAndAccountIfNeed())
|
||||||
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
|
||||||
.chain(() -> combRepository.persist(membreModel))
|
|
||||||
: Uni.createFrom().nullItem()
|
: Uni.createFrom().nullItem()
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@ -71,17 +90,24 @@ public class LicenceService {
|
|||||||
model.setValidate(form.isValidate());
|
model.setValidate(form.isValidate());
|
||||||
return Panache.withTransaction(() -> repository.persist(model)
|
return Panache.withTransaction(() -> repository.persist(model)
|
||||||
.call(m -> m.isValidate() ? Mutiny.fetch(m.getMembre())
|
.call(m -> m.isValidate() ? Mutiny.fetch(m.getMembre())
|
||||||
.call(membreModel -> (membreModel.getLicence() <= 0) ?
|
.call(genLicenceNumberAndAccountIfNeed())
|
||||||
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
|
||||||
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
|
||||||
.chain(() -> combRepository.persist(membreModel))
|
|
||||||
: Uni.createFrom().nullItem())
|
|
||||||
: Uni.createFrom().nullItem()
|
: Uni.createFrom().nullItem()
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() {
|
||||||
|
return membreModel -> ((membreModel.getLicence() <= 0) ?
|
||||||
|
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
||||||
|
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
||||||
|
.chain(() -> combRepository.persist(membreModel))
|
||||||
|
: Uni.createFrom().nullItem())
|
||||||
|
.call(__ -> (membreModel.getUserId() == null) ?
|
||||||
|
keycloakService.initCompte(membreModel.getId())
|
||||||
|
: Uni.createFrom().nullItem());
|
||||||
|
}
|
||||||
|
|
||||||
public Uni<?> deleteLicence(long id) {
|
public Uni<?> deleteLicence(long id) {
|
||||||
return Panache.withTransaction(() -> repository.deleteById(id));
|
return Panache.withTransaction(() -> repository.deleteById(id));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import jakarta.inject.Inject;
|
|||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||||
|
|
||||||
@ -97,6 +98,22 @@ public class LicenceEndpoints {
|
|||||||
return licenceService.setLicence(id, form).map(SimpleLicence::fromModel);
|
return licenceService.setLicence(id, form).map(SimpleLicence::fromModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("validate")
|
||||||
|
@RolesAllowed("federation_admin")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Validation licence", description = "Valide en masse les licence de l'année en cours (pour les administrateurs)")
|
||||||
|
@APIResponses(value = {
|
||||||
|
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
|
||||||
|
@APIResponse(responseCode = "403", description = "Accès refusé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
|
||||||
|
})
|
||||||
|
public Uni<?> valideLicences(@Parameter(description = "Id des membre a valider") List<Long> ids) {
|
||||||
|
return licenceService.valideLicences(ids);
|
||||||
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@RolesAllowed("federation_admin")
|
@RolesAllowed("federation_admin")
|
||||||
|
|||||||
302
src/main/webapp/src/pages/ValidateList.jsx
Normal file
302
src/main/webapp/src/pages/ValidateList.jsx
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import {useLoadingSwitcher} from "../hooks/useLoading.jsx";
|
||||||
|
import {useFetch} from "../hooks/useFetch.js";
|
||||||
|
import {AxiosError} from "../components/AxiosError.jsx";
|
||||||
|
import {ThreeDots} from "react-loader-spinner";
|
||||||
|
import {useEffect, useRef, useState} from "react";
|
||||||
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
|
import {apiAxios, errFormater} from "../utils/Tools.js";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {SearchBar} from "../components/SearchBar.jsx";
|
||||||
|
import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
|
||||||
|
|
||||||
|
export function ValidateList({source}) {
|
||||||
|
const {hash} = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
let page = Number(hash.substring(1));
|
||||||
|
page = (page > 0) ? page : 1;
|
||||||
|
|
||||||
|
const [memberData, setMemberData] = useState([]);
|
||||||
|
const [licenceData, setLicenceData] = useState([]);
|
||||||
|
const [clubFilter, setClubFilter] = useState("");
|
||||||
|
const [stateFilter, setStateFilter] = useState(2)
|
||||||
|
const [lastSearch, setLastSearch] = useState("");
|
||||||
|
|
||||||
|
const [selectedMembers, setSelectedMembers] = useState([]);
|
||||||
|
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}`, setLoading, 1)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}`);
|
||||||
|
}, [hash, clubFilter, stateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data)
|
||||||
|
return;
|
||||||
|
const data2 = [];
|
||||||
|
for (const e of data.result) {
|
||||||
|
data2.push({
|
||||||
|
id: e.id,
|
||||||
|
fname: e.fname,
|
||||||
|
lname: e.lname,
|
||||||
|
club: e.club,
|
||||||
|
categorie: e.categorie,
|
||||||
|
licence_number: e.licence,
|
||||||
|
licence: licenceData.find(licence => licence.membre === e.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setMemberData(data2);
|
||||||
|
}, [data, licenceData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.get(`/licence/current/${source}`),
|
||||||
|
{
|
||||||
|
pending: "Chargement des licences...",
|
||||||
|
success: "Licences chargées",
|
||||||
|
error: {
|
||||||
|
render({data}) {
|
||||||
|
return errFormater(data, "Impossible de charger les licences")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
setLicenceData(data.data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const search = (search) => {
|
||||||
|
if (search === lastSearch)
|
||||||
|
return;
|
||||||
|
setLastSearch(search);
|
||||||
|
refresh(`/member/find/${source}?page=${page}&search=${search}&club=${clubFilter}&licenceRequest=${stateFilter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidation = () => {
|
||||||
|
if (selectedMembers.length === 0) {
|
||||||
|
toast.error("Aucun membre sélectionné");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
apiAxios.post(`/licence/validate`, selectedMembers),
|
||||||
|
{
|
||||||
|
pending: "Validation des licences en cours...",
|
||||||
|
success: "Licences validées avec succès 🎉",
|
||||||
|
error: {
|
||||||
|
render({data}) {
|
||||||
|
return errFormater(data, "Échec de la validation des licences")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
setSelectedMembers([]);
|
||||||
|
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<h2>Validation des licences</h2>
|
||||||
|
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
|
||||||
|
« retour
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-9">
|
||||||
|
<SearchBar search={search}/>
|
||||||
|
{data
|
||||||
|
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate}
|
||||||
|
page={page} source={source} selectedMembers={selectedMembers} setSelectedMembers={setSelectedMembers}/>
|
||||||
|
: error
|
||||||
|
? <AxiosError error={error}/>
|
||||||
|
: <Def/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-3">
|
||||||
|
{source !== "club" &&
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">Filtre</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
|
||||||
|
stateFilter={stateFilter} setStateFilter={setStateFilter}/>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
{source === "admin" && <>
|
||||||
|
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation">Valider
|
||||||
|
les {selectedMembers.length} licences sélectionnée
|
||||||
|
</button>
|
||||||
|
<ConfirmDialog title="Validation des licences" message={"Êtes-vous sûr de vouloir valider les "+selectedMembers.length+" licences ?"}
|
||||||
|
onConfirm={handleValidation} id="confirm-validation"/>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeCentralPanel({data, visibleMember, navigate, page, source, selectedMembers, setSelectedMembers}) {
|
||||||
|
const lastCheckedRef = useRef(null);
|
||||||
|
|
||||||
|
function handleCheckbox(e, memberId) {
|
||||||
|
const isShiftKeyPressed = e.shiftKey;
|
||||||
|
const isChecked = !selectedMembers.includes(memberId); // Inverse l'état actuel
|
||||||
|
|
||||||
|
if (isShiftKeyPressed && lastCheckedRef.current !== null) {
|
||||||
|
// Sélection multiple avec Shift
|
||||||
|
const startIndex = visibleMember.findIndex(m => m.id === lastCheckedRef.current);
|
||||||
|
const endIndex = visibleMember.findIndex(m => m.id === memberId);
|
||||||
|
const [start, end] = [Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)];
|
||||||
|
|
||||||
|
const newSelected = [...selectedMembers];
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
const member = visibleMember[i];
|
||||||
|
if (isChecked && !newSelected.includes(member.id)) {
|
||||||
|
newSelected.push(member.id);
|
||||||
|
} else if (!isChecked) {
|
||||||
|
const index = newSelected.indexOf(member.id);
|
||||||
|
if (index !== -1) newSelected.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedMembers(newSelected);
|
||||||
|
} else {
|
||||||
|
// Sélection normale (sans Shift)
|
||||||
|
setSelectedMembers(prev =>
|
||||||
|
isChecked
|
||||||
|
? [...prev, memberId]
|
||||||
|
: prev.filter(id => id !== memberId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCheckedRef.current = memberId; // Met à jour le dernier membre cliqué
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCheckboxClick = (e, memberId) => {
|
||||||
|
handleCheckbox(e, memberId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (e, memberId) => {
|
||||||
|
// Si le clic est sur la checkbox, on laisse le gestionnaire de la checkbox gérer l'événement
|
||||||
|
if (e.target.type === 'checkbox') return;
|
||||||
|
handleCheckbox(e, memberId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages = []
|
||||||
|
for (let i = 1; i <= data.page_count; i++) {
|
||||||
|
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
|
||||||
|
<span className="page-link" onClick={() => navigate("#" + i)}>{i}</span>
|
||||||
|
</li>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="mb-4">
|
||||||
|
<small>Ligne {((page - 1) * data.page_size) + 1} à {
|
||||||
|
(page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size)} (page {page} sur {data.page_count})</small>
|
||||||
|
<ul className="list-group">
|
||||||
|
{visibleMember.map(member => (
|
||||||
|
<MakeRow key={member.id} member={member} navigate={navigate} source={source} isChecked={selectedMembers.includes(member.id)}
|
||||||
|
onCheckboxClick={handleCheckboxClick} onRowClick={handleRowClick}/>))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul className="pagination justify-content-center">
|
||||||
|
<li className={"page-item" + ((page <= 1) ? " disabled" : "")}>
|
||||||
|
<span className="page-link" onClick={() => navigate("#" + (page - 1))}>«</span></li>
|
||||||
|
{pages}
|
||||||
|
<li className={"page-item" + ((page >= data.page_count) ? " disabled" : "")}>
|
||||||
|
<span className="page-link" onClick={() => navigate("#" + (page + 1))}>»</span></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
|
||||||
|
const rowContent = <>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-auto">
|
||||||
|
<input className="form-check-input me-1" type="checkbox" checked={isChecked || false} onChange={() => {
|
||||||
|
}}
|
||||||
|
onClick={(e) => onCheckboxClick(e, member.id)}/>
|
||||||
|
<span>{member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="ms-2 col-auto">
|
||||||
|
<div className="fw-bold">{member.fname} {member.lname}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{source === "club" ?
|
||||||
|
<small>{member.categorie}</small>
|
||||||
|
: <small>{member.club?.name || "Sans club"}</small>}
|
||||||
|
</>
|
||||||
|
|
||||||
|
if (member.licence != null) {
|
||||||
|
return <li
|
||||||
|
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"))}
|
||||||
|
onClick={(e) => onRowClick(e, member.id)}>
|
||||||
|
{rowContent}
|
||||||
|
</li>
|
||||||
|
} else {
|
||||||
|
return <li className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
|
||||||
|
onClick={(e) => onRowClick(e, member.id)}>
|
||||||
|
{rowContent}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let allClub = []
|
||||||
|
|
||||||
|
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter}) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data)
|
||||||
|
return;
|
||||||
|
allClub.push(...data.result.map((e) => e.club?.name))
|
||||||
|
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
||||||
|
<div className="mb-3">
|
||||||
|
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
|
||||||
|
<option value={2}>Demande en cours</option>
|
||||||
|
<option value={5}>Demande complet</option>
|
||||||
|
<option value={6}>Demande incomplet</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClubSelectFilter({clubFilter, setClubFilter}) {
|
||||||
|
const setLoading = useLoadingSwitcher()
|
||||||
|
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{data
|
||||||
|
? <div className="mb-3">
|
||||||
|
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
||||||
|
<option value="">--- tout les clubs ---</option>
|
||||||
|
<option value="null">--- sans club ---</option>
|
||||||
|
{data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
: error
|
||||||
|
? <AxiosError error={error}/>
|
||||||
|
: <Def/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Def() {
|
||||||
|
return <div className="list-group">
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -9,8 +9,8 @@ import {AffiliationReqPage} from "./affiliation/AffiliationReqPage.jsx";
|
|||||||
import {NewClubPage} from "./club/NewClubPage.jsx";
|
import {NewClubPage} from "./club/NewClubPage.jsx";
|
||||||
import {ClubPage} from "./club/ClubPage.jsx";
|
import {ClubPage} from "./club/ClubPage.jsx";
|
||||||
import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx";
|
import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx";
|
||||||
import {Scale} from "leaflet/src/control/Control.Scale.js";
|
|
||||||
import {StatsPage} from "./StatsPage.jsx";
|
import {StatsPage} from "./StatsPage.jsx";
|
||||||
|
import {ValidateList} from "../ValidateList.jsx";
|
||||||
|
|
||||||
export function AdminRoot() {
|
export function AdminRoot() {
|
||||||
return <>
|
return <>
|
||||||
@ -35,6 +35,10 @@ export function getAdminChildren() {
|
|||||||
path: 'member/new',
|
path: 'member/new',
|
||||||
element: <NewMemberPage/>
|
element: <NewMemberPage/>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'member/validate',
|
||||||
|
element: <ValidateList source="admin"/>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'club',
|
path: 'club',
|
||||||
element: <ClubList/>
|
element: <ClubList/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user