feat: add membre list filter

This commit is contained in:
2024-02-06 21:39:42 +01:00
parent 40427b8cfb
commit 2fd6ef3c2e
15 changed files with 384 additions and 156 deletions

View File

@@ -77,4 +77,15 @@ export function CheckField({name, text, value, row = false}) {
</div>
}
</>
}
}
export const Checkbox = ({ label, value, onChange }) => {
const handleChange = () => {
onChange(!value);
};
return <div className="form-check">
<input className="form-check-input" type="checkbox" id="checkbox1" checked={value} onChange={handleChange}/>
<label className="form-check-label" htmlFor="checkbox1">{label}</label>
</div>
};

View File

@@ -19,12 +19,16 @@ export function useFetch(url, setLoading = null, loadingLevel = 1, config = {})
const [data, setData] = useState(null)
const [error, setErrors] = useState(null)
useEffect(() => {
const refresh = (url) => {
stdAction(apiAxios.get(url, config), setData, setErrors, setLoading, loadingLevel)
}
useEffect(() => {
refresh(url)
}, []);
return {
data, error
data, error, refresh
}
}

View File

@@ -0,0 +1,234 @@
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 {Input} from "../components/Input.jsx";
import {useLocation, useNavigate} from "react-router-dom";
import {Checkbox} from "../components/MemberCustomFiels.jsx";
import axios from "axios";
import {apiAxios} from "../utils/Tools.js";
import {toast} from "react-toastify";
const removeDiacritics = str => {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
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,
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: "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}/>
: 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>
</div>
</div>
</div>
</>
}
function SearchBar({search}) {
const [searchInput, setSearchInput] = useState("");
const handelChange = (e) => {
setSearchInput(e.target.value);
}
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
searchMember();
}
}
const searchMember = () => {
search(removeDiacritics(searchInput));
}
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
searchMember();
}, 750)
return () => clearTimeout(delayDebounceFn)
}, [searchInput])
return <div className="mb-3">
<div className="input-group mb-3">
<input type="text" className="form-control" placeholder="Rechercher..." aria-label="Rechercher..."
aria-describedby="button-addon2" value={searchInput} onChange={handelChange} onKeyDown={handleKeyDown}/>
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
onClick={searchMember}>Rechercher
</button>
</div>
</div>
}
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page}) {
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}/>))}
</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}) {
const rowContent = <>
<div className="row">
<span className="col-auto">{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>
<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" &&
<div className="mb-3">
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
<option value="">--- tout les clubs ---</option>
{allClub && allClub.map((value, index) => {
return <option key={index} value={value}>{value}</option>
})
}
</select>
</div>
}
</div>
}
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>
}

View File

@@ -1,7 +1,7 @@
import {Outlet} from "react-router-dom";
import './AdminRoot.css'
import {LoadingProvider} from "../../hooks/useLoading.jsx";
import {MemberList} from "./MemberList.jsx";
import {MemberList} from "../MemberList.jsx";
import {MemberPage} from "./member/MemberPage.jsx";
export function AdminRoot() {
@@ -17,7 +17,7 @@ export function getAdminChildren () {
return [
{
path: 'member',
element: <MemberList/>
element: <MemberList source="admin"/>
},
{
path: 'member/:id',

View File

@@ -1,60 +0,0 @@
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 {useState} from "react";
import {Input} from "../../components/Input.jsx";
import {useNavigate} from "react-router-dom";
const removeDiacritics = str => {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
export function MemberList() {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/all`, setLoading, 1)
const [searchInput, setSearchInput] = useState("");
const navigate = useNavigate();
const visibleMember = data ? data.filter(member => {
const lo = removeDiacritics(searchInput).toLowerCase()
return !searchInput
|| (removeDiacritics(member.fname).toLowerCase().startsWith(lo)
|| removeDiacritics(member.lname).toLowerCase().startsWith(lo));
}) : [];
return <>
<SearchBar searchInput={searchInput} onSearchInputChange={setSearchInput}/>
{data
? <div className="list-group">
{visibleMember.map(member => (
<span key={member.id}
onClick={() => navigate("/admin/member/" + member.id)}
className="list-group-item list-group-item-action">{member.fname} {member.lname}</span>))}
</div>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function SearchBar({searchInput, onSearchInputChange}) {
return <div>
<div className="mb-3">
<Input value={searchInput} onChange={onSearchInputChange} placeholder="Rechercher..."/>
</div>
</div>
}
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>
}

View File

@@ -19,7 +19,7 @@ export function MemberPage() {
return <>
<h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
&lt;&lt; retour
&laquo; retour
</button>
{data
? <div>

View File

@@ -1,8 +1,8 @@
import {Outlet} from "react-router-dom";
import {LoadingProvider} from "../../hooks/useLoading.jsx";
import {MemberList} from "./MemberList.jsx";
import {MemberPage} from "./member/MemberPage.jsx";
import {useAuth} from "../../hooks/useAuth.jsx";
import {MemberList} from "../MemberList.jsx";
export function ClubRoot() {
const {userinfo} = useAuth()
@@ -29,7 +29,7 @@ export function getClubChildren() {
return [
{
path: 'member',
element: <MemberList/>
element: <MemberList source="club"/>
},
{
path: 'member/:id',

View File

@@ -1,60 +0,0 @@
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 {useState} from "react";
import {Input} from "../../components/Input.jsx";
import {useNavigate} from "react-router-dom";
const removeDiacritics = str => {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
export function MemberList() {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/club`, setLoading, 1)
const [searchInput, setSearchInput] = useState("");
const navigate = useNavigate();
const visibleMember = data ? data.filter(member => {
const lo = removeDiacritics(searchInput).toLowerCase()
return !searchInput
|| (removeDiacritics(member.fname).toLowerCase().startsWith(lo)
|| removeDiacritics(member.lname).toLowerCase().startsWith(lo));
}) : [];
return <>
<SearchBar searchInput={searchInput} onSearchInputChange={setSearchInput}/>
{data
? <div className="list-group">
{visibleMember.map(member => (
<span key={member.id}
onClick={() => navigate("/club/member/" + member.id)}
className="list-group-item list-group-item-action">{member.fname} {member.lname}</span>))}
</div>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function SearchBar({searchInput, onSearchInputChange}) {
return <div>
<div className="mb-3">
<Input value={searchInput} onChange={onSearchInputChange} placeholder="Rechercher..."/>
</div>
</div>
}
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>
}

View File

@@ -18,7 +18,7 @@ export function MemberPage() {
return <>
<h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
&lt;&lt; retour
&laquo; retour
</button>
{data
? <div>