ffsaf-site/src/main/webapp/src/pages/MemberList.jsx
2025-07-05 14:11:05 +02:00

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))}>&laquo;</span></li>
{pages}
<li className={"page-item" + ((page >= data.page_count) ? " disabled" : "")}>
<span className="page-link" onClick={() => navigate("#" + (page + 1))}>&raquo;</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>
}