429 lines
17 KiB
JavaScript
429 lines
17 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} from "../utils/Tools.js";
|
|
import {toast} from "react-toastify";
|
|
import {SearchBar} from "../components/SearchBar.jsx";
|
|
import * as XLSX from "xlsx-js-style";
|
|
|
|
export function MemberList({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 [showLicenceState, setShowLicenceState] = useState(false);
|
|
const [clubFilter, setClubFilter] = useState("");
|
|
const [lastSearch, setLastSearch] = useState("");
|
|
|
|
const setLoading = useLoadingSwitcher()
|
|
const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}`, setLoading, 1)
|
|
|
|
|
|
useEffect(() => {
|
|
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}`);
|
|
}, [hash, clubFilter]);
|
|
|
|
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: 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 === lastSearch)
|
|
return;
|
|
setLastSearch(search);
|
|
refresh(`/member/find/${source}?page=${page}&search=${search}&club=${clubFilter}`);
|
|
}
|
|
|
|
return <>
|
|
<div>
|
|
<div className="row">
|
|
<div className="col-lg-9">
|
|
<SearchBar search={search}/>
|
|
{data
|
|
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState}
|
|
page={page} 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>
|
|
</div>
|
|
<div className="card mb-4">
|
|
<div className="card-header">Filtre</div>
|
|
<div className="card-body">
|
|
<FiltreBar showLicenceState={showLicenceState} setShowLicenceState={setShowLicenceState} data={data}
|
|
clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}/>
|
|
</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]) : '',
|
|
}
|
|
|
|
//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, 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={() => 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>
|
|
<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={() => 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, showLicenceState, navigate, source}) {
|
|
const rowContent = <>
|
|
<div className="row">
|
|
<span className="col-auto">{member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------"}</span>
|
|
<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 (showLicenceState && member.licence != null) {
|
|
return <div
|
|
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 ? "warning" : "danger"))}
|
|
onClick={() => navigate("" + member.id)}>{rowContent}</div>
|
|
} else {
|
|
return <div className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
|
|
onClick={() => navigate("" + member.id)}>
|
|
{rowContent}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
let allClub = []
|
|
|
|
function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source}) {
|
|
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>
|
|
<div className="mb-3">
|
|
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
|
|
</div>
|
|
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
|
</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>
|
|
}
|