609 lines
26 KiB
JavaScript
609 lines
26 KiB
JavaScript
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, useState} from "react";
|
|
import {useLocation, useNavigate} from "react-router-dom";
|
|
import {Checkbox} from "../components/MemberCustomFiels.jsx";
|
|
import * as Tools from "../utils/Tools.js";
|
|
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
|
|
import {toast} from "react-toastify";
|
|
import {SearchBar} from "../components/SearchBar.jsx";
|
|
import * as XLSX from "xlsx-js-style";
|
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|
import {faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
|
|
|
|
|
let lastRefresh = "";
|
|
|
|
export function MemberList({source}) {
|
|
const {hash} = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
const [memberData, setMemberData] = useState([]);
|
|
const [licenceData, setLicenceData] = useState([]);
|
|
const [showLicenceState, setShowLicenceState] = useState(false);
|
|
|
|
const setFilter = (filter) => {
|
|
navigate("#" + encodeURI(JSON.stringify(filter)))
|
|
}
|
|
const filter = {
|
|
page: 1,
|
|
search: "",
|
|
club: "",
|
|
licenceRequest: 4,
|
|
payment: 2,
|
|
order: "",
|
|
categorie: "",
|
|
archived: false,
|
|
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
|
|
}
|
|
|
|
const setLoading = useLoadingSwitcher()
|
|
const {
|
|
data,
|
|
error,
|
|
refresh
|
|
} = useFetch(`/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}&archive=${filter.archived}`, setLoading, 1)
|
|
|
|
useEffect(() => {
|
|
const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}&archive=${filter.archived}`;
|
|
if (tmp === lastRefresh)
|
|
return;
|
|
lastRefresh = tmp
|
|
refresh(lastRefresh);
|
|
}, [hash]);
|
|
|
|
useEffect(() => {
|
|
if (!data)
|
|
return;
|
|
if (data.page_count < filter.page) {
|
|
setFilter({...filter, page: 1});
|
|
}
|
|
|
|
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: showLicenceState ? licenceData.find(licence => licence.membre === e.id) : null
|
|
})
|
|
}
|
|
setMemberData(data2);
|
|
}, [data, licenceData]);
|
|
|
|
useEffect(() => {
|
|
if (!showLicenceState)
|
|
return;
|
|
|
|
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);
|
|
});
|
|
}, [showLicenceState]);
|
|
|
|
const search = (search) => {
|
|
if (search === filter.search)
|
|
return;
|
|
setFilter({...filter, search: search});
|
|
}
|
|
|
|
return <>
|
|
<div>
|
|
<div className="row">
|
|
<div className="col-lg-9">
|
|
<SearchBar search={search} defaultValue={filter.search}/>
|
|
{data
|
|
? <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/>
|
|
}
|
|
</div>
|
|
<div className="col-lg-3">
|
|
<div className="mb-4">
|
|
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un membre</button>
|
|
{source === "admin" &&
|
|
<button className="btn btn-primary" onClick={() => navigate("validate")} style={{marginTop: "0.5rem"}}>Valider des
|
|
licences</button>}
|
|
{source === "club" && false && // TODO: enable when payment is ready
|
|
<button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des
|
|
licences</button>}
|
|
</div>
|
|
|
|
<div className="card mb-4">
|
|
<div className="card-header">Trie</div>
|
|
<div className="card-body">
|
|
<OrderBar onOrderChange={e => setFilter({...filter, order: e.join(",")})} defaultValues={filter.order} source={source}/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card mb-4">
|
|
<div className="card-header">Filtre</div>
|
|
<div className="card-body">
|
|
<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}
|
|
stateFilter={filter.licenceRequest}
|
|
setStateFilter={e => setFilter({...filter, licenceRequest: e})}
|
|
paymentFilter={filter.payment}
|
|
setPaymentFilter={e => setFilter({...filter, payment: e})}
|
|
catFilter={filter.categorie}
|
|
setCatFilter={e => setFilter({...filter, categorie: e})}/>
|
|
</div>
|
|
</div>
|
|
|
|
{source === "club" &&
|
|
<div className="card mb-4">
|
|
<div className="card-header">Gestion groupée</div>
|
|
<div className="card-body">
|
|
<FileOutput/>
|
|
<div style={{marginTop: "1.5em"}}></div>
|
|
<FileInput/>
|
|
</div>
|
|
</div>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
}
|
|
|
|
function FileOutput() {
|
|
function formatColumnDate(worksheet, col) {
|
|
const range = XLSX.utils.decode_range(worksheet['!ref'])
|
|
// note: range.s.r + 1 skips the header row
|
|
for (let row = range.s.r + 1; row <= range.e.r; ++row) {
|
|
const ref = XLSX.utils.encode_cell({r: row, c: col})
|
|
if (worksheet[ref] && worksheet[ref].t === "n") {
|
|
worksheet[ref].v = Math.trunc(worksheet[ref].v)
|
|
} else {
|
|
worksheet[ref].t = "n"
|
|
}
|
|
worksheet[ref].z = "dd/mm/yyyy"
|
|
}
|
|
}
|
|
|
|
const handleFileDownload = () => {
|
|
toast.promise(
|
|
apiAxios.get(`/member/club/export`),
|
|
{
|
|
pending: "Exportation des licences...",
|
|
success: "Licences exportées",
|
|
error: {
|
|
render({data}) {
|
|
return errFormater(data, "Impossible d'exporté les licences")
|
|
}
|
|
}
|
|
})
|
|
.then(data => {
|
|
const dataOut = []
|
|
for (const e of data.data) {
|
|
const tmp = {
|
|
licence: e.licence,
|
|
nom: e.nom,
|
|
prenom: e.prenom,
|
|
email: e.email,
|
|
genre: e.genre,
|
|
birthdate: new Date(e.birthdate),
|
|
licenceCurrent: e.licenceCurrent ? 'X' : '',
|
|
certif: e.certif ? e.certif.split("¤")[0] : '',
|
|
certifDate: e.certif ? new Date(e.certif.split("¤")[1]) : '',
|
|
}
|
|
|
|
if (isNaN(tmp.certifDate) || tmp.certifDate === 'NaN') {
|
|
tmp.certifDate = ''
|
|
}
|
|
|
|
//tmp.birthdate.setMilliseconds(0);
|
|
//tmp.birthdate.setSeconds(0);
|
|
//tmp.birthdate.setMinutes(0);
|
|
//tmp.birthdate.setHours(0);
|
|
//
|
|
//console.log(tmp.birthdate);
|
|
dataOut.push(tmp)
|
|
}
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
const ws = XLSX.utils.json_to_sheet(dataOut);
|
|
XLSX.utils.sheet_add_aoa(ws, [["Licence", "Nom", "Prénom", "Email", "Genre", "Date de naissance", "Licence en cours", "Nom médecin certificat", "Date certificat"]], {origin: 'A1'});
|
|
// XLSX.utils.sheet_add_json(ws, dataOut, {skipHeader: true, origin: 'A2'});
|
|
|
|
formatColumnDate(ws, 5)
|
|
formatColumnDate(ws, 8)
|
|
console.log(ws)
|
|
//ws["!data"][0][0].z = "yyyy-mm-dd hh:mm:ss"
|
|
ws["!cols"] = [{wch: 7}, {wch: 16}, {wch: 16}, {wch: 30}, {wch: 9}, {wch: 12}, {wch: 6}, {wch: 13}, {wch: 12}]
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, `Saison ${Tools.getSaison()}-${Tools.getSaison() + 1}`);
|
|
XLSX.writeFile(wb, "output.xlsx");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<button className="btn btn-primary" onClick={handleFileDownload}>Télécharger l'Excel des membres</button>
|
|
<small>À utiliser comme template pour mettre à jour les informations</small>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
function FileInput() {
|
|
const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
|
|
|
|
function excelDateToJSDate(serial) {
|
|
const utcDays = Math.floor(serial - 25569);
|
|
const utcValue = utcDays * 86400;
|
|
return new Date(utcValue * 1000);
|
|
}
|
|
|
|
const handleFileUpload = (e) => {
|
|
const file = e.target.files[0];
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (event) => {
|
|
const workbook = XLSX.read(event.target.result, {type: 'binary'});
|
|
const sheet = workbook.Sheets[`Saison ${Tools.getSaison()}-${Tools.getSaison() + 1}`];
|
|
const sheetData = XLSX.utils.sheet_to_json(sheet);
|
|
|
|
const dataOut = []
|
|
let error = 0;
|
|
let cetifNotFill = 0;
|
|
for (let i = 0; i < sheetData.length; i++) {
|
|
const line = sheetData[i];
|
|
// noinspection NonAsciiCharacters,JSNonASCIINames
|
|
const tmp = {
|
|
licence: line["Licence"],
|
|
nom: line["Nom"],
|
|
prenom: line["Prénom"],
|
|
email: line["Email"],
|
|
genre: line["Genre"],
|
|
birthdate: line["Date de naissance"],
|
|
licenceCurrent: line["Licence en cours"] === undefined ? false : line["Licence en cours"].toLowerCase() === "x",
|
|
certif: "",
|
|
}
|
|
|
|
if (tmp.nom === undefined || tmp.nom === "") {
|
|
toast.error("Nom vide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
if (tmp.prenom === undefined || tmp.prenom === "") {
|
|
toast.error("Prénom vide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
|
|
if (tmp.licenceCurrent) { // need check full data
|
|
if (tmp.email === undefined || tmp.email === "") {
|
|
toast.error("Email vide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
if (!re.test(tmp.email)) {
|
|
toast.error("Email invalide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
// noinspection NonAsciiCharacters,JSNonASCIINames
|
|
if (line["Nom médecin certificat"] === undefined || line["Nom médecin certificat"] === "" ||
|
|
line["Date certificat"] === undefined || line["Date certificat"] === "") {
|
|
cetifNotFill++;
|
|
} else {
|
|
try {
|
|
const date = excelDateToJSDate(line["Date certificat"]);
|
|
if (Number.isNaN(date.getFullYear())) {
|
|
toast.error("Format de la date de certificat invalide à la ligne " + (i + 2))
|
|
error++;
|
|
} else {
|
|
// noinspection JSNonASCIINames
|
|
tmp.certif = line["Nom médecin certificat"] + "¤" + date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2);
|
|
}
|
|
} catch (e) {
|
|
toast.error("Format de la date de certificat invalide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
}
|
|
|
|
if (tmp.birthdate === undefined || tmp.birthdate === "") {
|
|
toast.error("Date de naissance vide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
}
|
|
|
|
if (tmp.birthdate !== undefined && tmp.birthdate !== "") {
|
|
console.log(tmp.birthdate);
|
|
try {
|
|
tmp.birthdate = excelDateToJSDate(tmp.birthdate).toISOString();
|
|
} catch (e) {
|
|
toast.error("Format de la date de naissance invalide à la ligne " + (i + 2))
|
|
error++;
|
|
}
|
|
}
|
|
|
|
dataOut.push(tmp)
|
|
}
|
|
|
|
if (error > 0) {
|
|
toast.error(`${error} erreur(s) dans le fichier, opération annulée`)
|
|
} else {
|
|
console.log(dataOut);
|
|
|
|
toast.promise(
|
|
apiAxios.put(`/member/club/import`, dataOut),
|
|
{
|
|
pending: "Envoie des changement en cours",
|
|
success: "Changement envoyé avec succès 🎉",
|
|
error: {
|
|
render({data}) {
|
|
return errFormater(data, "Échec de l'envoie des changements")
|
|
}
|
|
}
|
|
}
|
|
).then(_ => {
|
|
if (cetifNotFill > 0)
|
|
toast.warn(`${cetifNotFill} certificat(s) médical(aux) non rempli(s)`)
|
|
})
|
|
}
|
|
};
|
|
|
|
reader.readAsBinaryString(file);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<span>Charger l'Excel</span>
|
|
<div className="input-group">
|
|
<input type="file" className="form-control" id="logo" name="logo" accept=".xls,.xlsx" onChange={handleFileUpload}/>
|
|
</div>
|
|
<small>Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.</small>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) {
|
|
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={() => setPage(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>
|
|
<div className="list-group">
|
|
{visibleMember.map(member => (
|
|
<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))}
|
|
</div>
|
|
</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={() => setPage(page - 1)}>«</span></li>
|
|
{pages}
|
|
<li className={"page-item" + ((page >= data.page_count) ? " disabled" : "")}>
|
|
<span className="page-link" onClick={() => setPage(page + 1)}>»</span></li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</>
|
|
}
|
|
|
|
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') : "-------") + " "}
|
|
{(showLicenceState && member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <> </>}</span>
|
|
<div className="ms-2 col-auto">
|
|
<div className="fw-bold">{member.fname} {member.lname}</div>
|
|
</div>
|
|
</div>
|
|
<div style={{verticalAlign: "center", margin: "auto 0"}}>
|
|
{source === "club" ?
|
|
<small>{getCatName(member.categorie)}</small>
|
|
: <div style={{
|
|
textAlign: "right",
|
|
fontSize: "small"
|
|
}}>{member.club?.name || "Sans club"}<br/><small>{getCatName(member.categorie)}</small></div>}
|
|
</div>
|
|
|
|
</>
|
|
|
|
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"}}
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
navigate("" + member.id)
|
|
}}
|
|
href={"member/" + member.id}>
|
|
{rowContent}
|
|
</a>
|
|
} else {
|
|
return <a className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
|
|
style={{padding: "0 1em"}}
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
navigate("" + member.id)
|
|
}}
|
|
href={"member/" + member.id}>
|
|
{rowContent}
|
|
</a>
|
|
}
|
|
}
|
|
|
|
function OrderBar({onOrderChange, defaultValues = "", source}) {
|
|
const [orderCriteria, setOrderCriteria] = useState([...defaultValues.split(",").filter(c => c !== ''), '']);
|
|
|
|
const handleChange = (index, value) => {
|
|
const newCriteria = [...orderCriteria];
|
|
newCriteria[index] = value;
|
|
|
|
// Si le dernier critère est rempli, on en ajoute un nouveau
|
|
if (index === orderCriteria.length - 1 && value !== '') {
|
|
newCriteria.push('');
|
|
}
|
|
// Si un critère (sauf le premier) est réinitialisé, on le supprime
|
|
else if (value === '' && (index !== 0 || orderCriteria.length > 1)) {
|
|
newCriteria.splice(index, 1);
|
|
}
|
|
|
|
setOrderCriteria(newCriteria);
|
|
onOrderChange(newCriteria.filter(c => c !== ''));
|
|
};
|
|
|
|
// Liste de toutes les options possibles
|
|
const allOptions = [
|
|
{value: 'lname n', label: 'Nom ↓', base: 'lname'},
|
|
{value: 'lname i', label: 'Nom ↑', base: 'lname'},
|
|
{value: 'fname n', label: 'Prénom ↓', base: 'fname'},
|
|
{value: 'fname i', label: 'Prénom ↑', base: 'fname'},
|
|
{value: 'categorie n', label: 'Catégorie ↓', base: 'categorie'},
|
|
{value: 'categorie i', label: 'Catégorie ↑', base: 'categorie'},
|
|
{value: 'licence n', label: 'Licence ↓', base: 'licence'},
|
|
{value: 'licence i', label: 'Licence ↑', base: 'licence'},
|
|
];
|
|
|
|
if (source === "admin") {
|
|
allOptions.push(
|
|
{value: 'club.name n', label: 'Club ↓', base: 'club.name'},
|
|
{value: 'club.name i', label: 'Club ↑', base: 'club.name'},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mb-3">
|
|
{orderCriteria.map((criteria, index) => {
|
|
// Récupère les bases des critères déjà sélectionnés (sauf le courant)
|
|
const usedBases = orderCriteria
|
|
.filter((c, i) => c !== '' && i !== index)
|
|
.map(c => allOptions.find(o => o.value === c)?.base);
|
|
|
|
// Filtre les options disponibles
|
|
const availableOptions = allOptions.filter(option =>
|
|
!usedBases.includes(option.base) || option.value === criteria
|
|
);
|
|
|
|
return (
|
|
<select
|
|
key={index}
|
|
className="form-select mb-2"
|
|
value={criteria}
|
|
onChange={(e) => handleChange(index, e.target.value)}
|
|
>
|
|
<option value="">----</option>
|
|
{availableOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FiltreBar({
|
|
showLicenceState,
|
|
setShowLicenceState,
|
|
showArchived,
|
|
setShowArchived,
|
|
clubFilter,
|
|
setClubFilter,
|
|
source,
|
|
stateFilter,
|
|
setStateFilter,
|
|
paymentFilter,
|
|
setPaymentFilter,
|
|
catFilter,
|
|
setCatFilter,
|
|
}) {
|
|
|
|
return <div>
|
|
<div className="mb-3">
|
|
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label="Afficher les combattants inactifs"/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
|
<option value="">--- toute les catégories ---</option>
|
|
{Tools.CatList.map(cat => (
|
|
<option key={cat} value={cat}>{getCatName(cat)}</option>
|
|
))}
|
|
</select>
|
|
</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={4}>Tout les états de licences</option>
|
|
<option value={0}>Sans demande ni licence validée</option>
|
|
<option value={1}>Avec demande ou licence validée</option>
|
|
<option value={2}>Demande en cours</option>
|
|
<option value={5}>Demande complet</option>
|
|
<option value={6}>Demande incomplet</option>
|
|
<option value={3}>Licence validée</option>
|
|
</select>
|
|
</div>
|
|
<div className="mb-3">
|
|
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}
|
|
hidden={stateFilter === 0 || stateFilter === 4}>
|
|
<option value={2}>Tout les états de paiement</option>
|
|
<option value={0}>Sans paiement</option>
|
|
<option value={1}>Avec paiement</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>
|
|
}
|