wip: club

This commit is contained in:
2024-07-11 23:02:29 +02:00
parent d5b4820c79
commit 76d7a28678
33 changed files with 1346 additions and 227 deletions

View File

@@ -14,6 +14,10 @@
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -15,8 +15,10 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.21.2",
"react-toastify": "^10.0.4"
@@ -1023,6 +1025,16 @@
"node": ">= 8"
}
},
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz",
@@ -3113,6 +3125,11 @@
"json-buffer": "3.0.1"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -3558,6 +3575,19 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-leaflet": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
"dependencies": {
"@react-leaflet/core": "^2.1.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/react-loader-spinner": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz",

View File

@@ -17,8 +17,10 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.21.2",
"react-toastify": "^10.0.4"

View File

@@ -36,7 +36,7 @@ export function BirthDayField({inti_date, inti_category}) {
</>
}
export function OptionField({name, text, values, value, disabled=false}) {
export function OptionField({name, text, values, value, disabled = false}) {
return <div className="row">
<div className="input-group mb-3">
<label className="input-group-text" id={name}>{text}</label>
@@ -49,12 +49,20 @@ export function OptionField({name, text, values, value, disabled=false}) {
</div>
}
export function TextField({name, text, value, placeholder, type = "text"}) {
export function CountryList({name, text, value, values = undefined, disabled = false}) {
if (values === undefined){
values = {NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}
}
return <OptionField name={name} text={text} value={value} values={values} disabled={disabled}/>
}
export function TextField({name, text, value, placeholder, type = "text", disabled = false}) {
return <div className="row">
<div className="input-group mb-3">
<span className="input-group-text" id={name}>{text}</span>
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
name={name} aria-describedby={name} defaultValue={value} required/>
name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required/>
</div>
</div>
}
@@ -79,7 +87,7 @@ export function CheckField({name, text, value, row = false}) {
</>
}
export const Checkbox = ({ label, value, onChange }) => {
export const Checkbox = ({label, value, onChange}) => {
const handleChange = () => {
onChange(!value);
};

View File

@@ -72,7 +72,7 @@ function AdminMenu() {
</div>
<ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Member</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/b">B</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/club">Club</NavLink></li>
</ul>
</li>
}

View File

@@ -0,0 +1,43 @@
import {useEffect, useState} from "react";
const removeDiacritics = str => {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
export 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>
}

View File

@@ -30,15 +30,16 @@ export function DemandeAff() {
const submit = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
formData.append("m1_role", event.target.m1_role?.value)
toast.promise(
apiAxios.post(`asso/affiliation`, formData, { headers: {'Accept': '*/*'}}),
apiAxios.post(`/affiliation`, formData, {headers: {'Accept': '*/*'}}),
{
pending: "Enregistrement de la demande d'affiliation en cours",
success: "Demande d'affiliation enregistrée avec succès 🎉",
error: "Échec de la demande d'affiliation 😕"
}
).then(_ => {
// navigate("/affiliation/ok")
navigate("/affiliation/ok")
})
}
@@ -67,14 +68,14 @@ export function DemandeAff() {
<div className="card-body">
<h4>L'association</h4>
<AssoInfo/>
<h4>Le président</h4>
<MembreInfo role="president"/>
<h4>Le trésorier</h4>
<MembreInfo role="tresorier"/>
<h4>Le secrétaire</h4>
<MembreInfo role="secretaire"/>
<h4>Membre n°1</h4>
<MembreInfo role="m1"/>
<h4 style={{marginTop: '1em'}}>Membre n°2</h4>
<MembreInfo role="m2"/>
<h4 style={{marginTop: '1em'}}>Membre n°3</h4>
<MembreInfo role="m3"/>
<div className="mb-3">
<div className="mb-3" style={{marginTop: '1em'}}>
<p>Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour
accéder à votre espace FFSAF</p>
Notez que pour finaliser votre affiliation, il vous faudra :
@@ -126,13 +127,13 @@ function AssoInfo() {
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span>
<input type="text" className="form-control" placeholder="Nom de l'association" name="name"
aria-label="Nom de l'association"
aria-describedby="basic-addon1" required/>
aria-describedby="basic-addon1" required defaultValue="Mesnie"/>
</div>
<div className="input-group mb-3">
<span className="input-group-text">N° SIREN*</span>
<input type="number" className="form-control" placeholder="siren" name="siren" required value={siren}
onChange={e => setSiren(e.target.value)}/>
onChange={e => setSiren(e.target.value)} defaultValue={500213731}/>
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
onClick={fetchSiren}>Rechercher
</button>
@@ -173,36 +174,65 @@ function AssoInfo() {
}
function MembreInfo({role}) {
return <div className="row g-3 mb-3">
<div className="col-sm-3">
<div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Nom" name={role + "-nom"}/>
<label htmlFor="floatingInput">Nom</label>
const [switchOn, setSwitchOn] = useState(false);
return <>
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôles</label>
<select className="form-select" id="inputGroupSelect01" defaultValue={role === "m1" ? "PRESIDENT" : 0}
disabled={role === "m1"} name={role + "_role"} required>
<option>Sélectionner...</option>
<option value="PRESIDENT">Président</option>
<option value="TRESORIER">Trésorier</option>
<option value="SECRETAIRE">Secrétaire</option>
<option value="VPRESIDENT">Vise-Président</option>
<option value="VTRESORIER">Vise-Trésorier</option>
<option value="VSECRETAIRE">Vise-Secrétaire</option>
<option value="MEMBREBUREAU">Membre du bureau</option>
</select>
</div>
<div className="row g-3 mb-3">
<div className="col-sm-3">
<div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Nom" name={role + "_nom"} defaultValue={role + "-nom"} required/>
<label htmlFor="floatingInput">Nom</label>
</div>
</div>
<div className="col-sm-3">
<div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom"
name={role + "_prenom"} defaultValue={role + "_prenom"} required/>
<label htmlFor="floatingInput">Prénom</label>
</div>
</div>
<div className="col-sm-5">
<div className="form-floating">
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com"
name={role + "_mail"} defaultValue={role + "-mail@test.com"} required/>
<label htmlFor="floatingInput">Email</label>
</div>
</div>
</div>
<div className="col-sm-3">
<div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom"
name={role + "-prenom"}/>
<label htmlFor="floatingInput">Prénom</label>
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Dispose déjà d'une licence</label>
<div className="input-group-text">
<input type="checkbox" id="inputGroupSelect01" className="form-check-input mt-0"
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
</div>
</div>
<div className="col-sm-5">
<div className="form-floating">
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com"
name={role + "-mail"}/>
<label htmlFor="floatingInput">Email</label>
{switchOn &&
<div className="col-sm-3">
<div className="form-floating">
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
name={role + "_licence"}/>
<label htmlFor="floatingInput">N° Licence</label>
</div>
</div>
</div>
<div className="col-sm-3">
<div>OU</div>
<div className="form-floating">
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
name={role + "-licence"}/>
<label htmlFor="floatingInput">N° Licence</label>
</div>
</div>
</div>
}
</>
}
export function DemandeAffOk() {

View File

@@ -9,6 +9,7 @@ import {Checkbox} from "../components/MemberCustomFiels.jsx";
import axios from "axios";
import {apiAxios} from "../utils/Tools.js";
import {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx";
const removeDiacritics = str => {
return str
@@ -106,41 +107,6 @@ export function MemberList({source}) {
</>
}
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++) {

View File

@@ -4,6 +4,10 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx";
import {MemberList} from "../MemberList.jsx";
import {MemberPage} from "./member/MemberPage.jsx";
import {NewMemberPage} from "./member/NewMemberPage.jsx";
import {ClubList} from "./club/ClubList.jsx";
import {AffiliationReqPage} from "./affiliation/AffiliationReqPage.jsx";
import {NewClubPage} from "./club/NewClubPage.jsx";
import {ClubPage} from "./club/ClubPage.jsx";
export function AdminRoot() {
return <>
@@ -28,6 +32,22 @@ export function getAdminChildren() {
path: 'member/new',
element: <NewMemberPage/>
},
{
path: 'club',
element: <ClubList/>
},
{
path: 'club/:id',
element: <ClubPage/>
},
{
path: 'affiliation/request',
element: <AffiliationReqPage/>
},
{
path: 'club/new',
element: <NewClubPage/>
},
{
path: 'b',
element: <div>Admin B</div>

View File

@@ -0,0 +1,16 @@
import {useNavigate} from "react-router-dom";
export function AffiliationReqPage() {
const navigate = useNavigate();
return <>
<h2>Page affiliation</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/affiliation")}>
&laquo; retour
</button>
<div>
<div className="row">
</div>
</div>
</>
}

View File

@@ -0,0 +1,187 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {apiAxios, getSaison} from "../../../utils/Tools.js";
import {Input} from "../../../components/Input.jsx";
import {toast} from "react-toastify";
function affiliationReducer(affiliation, action) {
switch (action.type) {
case 'ADD':
return [
...affiliation,
action.payload
]
case 'REMOVE':
return affiliation.filter(affiliation => affiliation.id !== action.payload)
case 'UPDATE_OR_ADD':
const index = affiliation.findIndex(affiliation => affiliation.id === action.payload.id)
if (index === -1) {
return [
...affiliation,
action.payload
]
} else {
affiliation[index] = action.payload
return [...affiliation]
}
case 'SORT':
return affiliation.sort((a, b) => b.saison - a.saison)
default:
throw new Error()
}
}
export function AffiliationCard({clubData}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1)
const [modalAffiliation, setModal] = useState({id: -1, club: clubData.id})
const [affiliations, dispatch] = useReducer(affiliationReducer, [])
useEffect(() => {
if (!data) return
for (const dataKey of data) {
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
}
dispatch({type: 'SORT'})
}, [data]);
return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid">
<div className="row">
<div className="col">Affiliation</div>
<div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#AffiliationModal"
onClick={_ => setModal({id: -1, club: clubData.id})}>Ajouter
</button>
</div>
</div>
</div>
<div className="card-body">
<ul className="list-group">
{affiliations.map((affiliation, index) => {
return <div key={index}
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
(affiliation.validate ? "success" : "warning")}>
<div className="me-auto">{affiliation?.saison}-{affiliation?.saison + 1}</div>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#AffiliationModal" onClick={_ => setModal(affiliation)}>
<FontAwesomeIcon icon={faPen}/></button>
</div>
})}
{error && <AxiosError error={error}/>}
</ul>
</div>
<div className="modal fade" id="AffiliationModal" tabIndex="-1" aria-labelledby="AffiliationModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<ModalContent affiliation={modalAffiliation} dispatch={dispatch}/>
</div>
</div>
</div>
</div>;
}
function sendAffiliation(event, dispatch) {
event.preventDefault();
const formData = new FormData(event.target);
toast.promise(
apiAxios.post(`/affiliation/${formData.get('membre')}`, formData), // TODO
{
pending: "Enregistrement de l'affiliation en cours",
success: "Affiliation enregistrée avec succès 🎉",
error: "Échec de l'enregistrement de l'affiliation 😕"
}
).then(data => {
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
dispatch({type: 'SORT'})
})
}
function removeAffiliation(id, dispatch) {
toast.promise(
apiAxios.delete(`/affiliation/${id}`),
{
pending: "Suppression de l'affiliation en cours",
success: "Affiliation supprimée avec succès 🎉",
error: "Échec de la suppression de l'affiliation 😕"
}
).then(_ => {
dispatch({type: 'REMOVE', payload: id})
})
}
function ModalContent({affiliation, dispatch}) {
const [saison, setSaison] = useState(0)
const [validate, setValidate] = useState(false)
const [isNew, setNew] = useState(true)
const setSeason = (event) => {
setSaison(Number(event.target.value))
}
const handleValidateChange = (event) => {
setValidate(event.target.value === 'true');
}
useEffect(() => {
if (affiliation.id !== -1) {
setNew(false)
setSaison(affiliation.saison)
setValidate(affiliation.validate)
} else {
setNew(true)
setSaison(getSaison())
setValidate(false)
}
}, [affiliation]);
return <form onSubmit={e => sendAffiliation(e, dispatch)}>
<input name="id" value={affiliation.id} readOnly hidden/>
<input name="membre" value={affiliation.membre} readOnly hidden/>
<div className="modal-header">
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Edition de l'affiliation</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="input-group mb-3 justify-content-md-center">
{isNew
? <input type="number" className="form-control" placeholder="Saison" name="saison"
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
: <><span className="input-group-text" id="basic-addon2">{saison}</span>
<input name="saison" value={saison} readOnly hidden/></>}
<span className="input-group-text" id="basic-addon2">-</span>
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
</div>
<RadioGroupeOnOff name="validate" text="Validation de l'affiliation" value={validate}
onChange={handleValidateChange}/>
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
onClick={() => removeAffiliation(affiliation.id, dispatch)}>Supprimer</button>}
</div>
</form>
}
function RadioGroupeOnOff({value, onChange, name, text}) {
return <div className="btn-group input-group mb-3 justify-content-md-center" role="group"
aria-label="Basic radio toggle button group">
<span className="input-group-text">{text}</span>
<input type="radio" className="btn-check" id={"btnradio1" + name} autoComplete="off"
value="false" checked={value === false} onChange={onChange}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + name}>Non</label>
<input type="radio" className="btn-check" name={name} id={"btnradio2" + name} autoComplete="off"
value="true" checked={value === true} onChange={onChange}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + name}>Oui</label>
</div>;
}

View File

@@ -0,0 +1,192 @@
import {useLocation, useNavigate} from "react-router-dom";
import {useEffect, useState} from "react";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify";
import {apiAxios} from "../../../utils/Tools.js";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {Checkbox} from "../../../components/MemberCustomFiels.jsx";
import {ThreeDots} from "react-loader-spinner";
import {SearchBar} from "../../../components/SearchBar.jsx";
export function ClubList() {
const {hash} = useLocation();
const navigate = useNavigate();
let page = Number(hash.substring(1));
page = (page > 0) ? page : 1;
const [clubData, setClubData] = useState([]);
const [affiliationData, setAffiliationData] = useState([]);
const [showAffiliationState, setShowAffiliationState] = useState(false);
const [countryFilter, setCountryFilter] = useState("");
const [lastSearch, setLastSearch] = useState("");
const setLoading = useLoadingSwitcher()
const {data, error, refresh} = useFetch(`/club/find?page=${page}`, setLoading, 1)
useEffect(() => {
refresh(`/club/find?page=${page}&search=${lastSearch}&country=${countryFilter}`);
}, [hash, countryFilter]);
useEffect(() => {
if (!data)
return;
const data2 = [];
for (const e of data.result) {
data2.push({
id: e.id,
name: e.name,
country: e.country,
shieldURL: e.shieldURL,
no_affiliation: e.no_affiliation,
affiliation: showAffiliationState ? affiliationData.find(licence => licence.club === e.id) : null
})
}
setClubData(data2);
}, [data, affiliationData]);
useEffect(() => {
if (!showAffiliationState)
return;
toast.promise(
apiAxios.get(`/affiliation/current`),
{
pending: "Chargement des affiliation...",
success: "Affiliation chargées",
error: "Impossible de charger les affiliations"
})
.then(data => {
setAffiliationData(data.data);
});
}, [showAffiliationState]);
const search = (search) => {
if (search === lastSearch)
return;
setLastSearch(search);
refresh(`/club/find?page=${page}&search=${search}&country=${countryFilter}`);
}
return <>
<h2>Club</h2>
<div>
<div className="row">
<div className="col-lg-9">
<SearchBar search={search}/>
{data
? <MakeCentralPanel data={data} visibleclub={clubData} navigate={navigate} showLicenceState={showAffiliationState}
page={page}/>
: error
? <AxiosError error={error}/>
: <Def/>
}
</div>
<div className="col-lg-3">
<div className="mb-4">
<button className="btn btn-primary" onClick={() => navigate("../affiliation/request")}>Demande en cours</button>
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter une affiliation</button>
</div>
<div className="card mb-4">
<div className="card-header">Filtre</div>
<div className="card-body">
<FiltreBar showAffiliationState={showAffiliationState} setShowLAffiliationState={setShowAffiliationState} data={data}
countryFilter={countryFilter} setCountryFilter={setCountryFilter}/>
</div>
</div>
</div>
</div>
</div>
</>
}
function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, 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">
{visibleclub.map(club => (<MakeRow key={club.id} club={club} navigate={navigate} showAffiliationState={showAffiliationState}/>))}
</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({club, showAffiliationState, navigate}) {
const rowContent = <>
<div className="row">
<span className="col-auto">{String(club.no_affiliation).padStart(5, '0')}</span>
<div className="ms-2 col-auto">
<div className="fw-bold">{club.name}</div>
</div>
</div>
<small>{club.country}</small>
</>
if (showAffiliationState && club.affiliation != null) {
return <div
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-"
+ (club.affiliation.validate ? "success" : "warning")}
onClick={() => navigate("" + club.id)}>{rowContent}</div>
} else {
return <div className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
onClick={() => navigate("" + club.id)}>
{rowContent}
</div>
}
}
let allCountry = []
function FiltreBar({showAffiliationState, setShowAffiliationState, data, countryFilter, setCountryFilter}) {
useEffect(() => {
if (!data)
return;
allCountry.push(...data.result.map((e) => e.club?.name))
allCountry = allCountry.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
}, [data]);
return <div>
<div className="mb-3">
<Checkbox value={showAffiliationState} onChange={setShowAffiliationState} label="Afficher l'état des affiliation"/>
</div>
<div className="mb-3">
<select className="form-select" value={countryFilter} onChange={event => setCountryFilter(event.target.value)}>
<option value="">--- tout les pays ---</option>
{allCountry && allCountry.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

@@ -0,0 +1,128 @@
import {useNavigate, useParams} from "react-router-dom";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify";
import {apiAxios} from "../../../utils/Tools.js";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {AffiliationCard} from "./AffiliationCard.jsx";
import {CheckField, CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {MapContainer, Marker, Popup, TileLayer, useMap} from 'react-leaflet'
const vite_url = import.meta.env.VITE_URL;
export function ClubPage() {
const {id} = useParams()
const navigate = useNavigate();
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/${id}`, setLoading, 1)
const handleRm = () => {
toast.promise(
apiAxios.delete(`/club/${id}`),
{
pending: "Suppression du club en cours...",
success: "Club supprimé avec succès 🎉",
error: "Échec de la suppression du club 😕"
}
).then(_ => {
navigate("/admin/club")
})
}
return <>
<h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
&laquo; retour
</button>
{data
? <div>
<div className="row">
<div className="col-lg-8">
<LoadingProvider><InformationForm data={data}/></LoadingProvider>
</div>
<div className="col-lg-4">
<LoadingProvider><AffiliationCard clubData={data}/></LoadingProvider>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte
</button>
</div>
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?"
onConfirm={handleRm}/>
</div>
</div>
</div>
: error && <AxiosError error={error}/>
}
</>
}
function InformationForm({data}) {
return <div className="card mb-4">
<div className="card-header">Licence n°{data.no_affiliation}</div>
<div className="card-body text-center">
<TextField name="clubId" text="ClubID" value={data.clubId} disabled={true}/>
<TextField name="name" text="Nom" value={data.name}/>
<TextField name="siret" text="SIRET" value={data.siret} type="number"/>
<TextField name="rna" text="RNA" value={data.rna}/>
<CountryList name="country" text="Pays" value={data.country}/>
<img
src={`${vite_url}/api/club/${data.id}/logo`}
alt="avatar"
className="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
<div className="mb-3">
<div className="input-group">
<label className="input-group-text" htmlFor="url_photo">Blason</label>
<input type="file" className="form-control" id="url_photo" name="url_photo"
accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div>
<div className="form-text" id="url_photo">Laissez vide pour ne rien changer.</div>
</div>
<TextField name="contact" text="Contact" value={data.contact}/>
<TextField name="training_location" text="Lieux d'entrainement" value={data.training_location}/>
<TextField name="training_day_time" text="Horaire d'entrainement" value={data.training_day_time}/>
<TextField name="contact_intern" text="Contact" value={"contact_intern"}/>
<CheckField name="international" text="Club international" value={data.international}/>
<MainMap/>
</div>
</div>;
}
// https://annuaire-entreprises.data.gouv.fr/entreprise/la-mesnie-des-chevaliers-de-st-georges-et-de-st-michel-500213731
const position = [51.505, -0.09]
function MainMap() {
function handleReturnCurrentPosition() {
console.log("I have clicked return button!!");
//const newCurrentPositionId = uuidv4();
//setReturnCurrentPosition(newCurrentPositionId);
//console.log(newCurrentPositionId);
}
return (
<>
<MapContainer center={position} zoom={13} scrollWheelZoom={false} style={{height: "30em", width: "50em"}}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={position}>
<Popup>
A pretty CSS3 popup. <br/> Easily customizable.
</Popup>
</Marker>
</MapContainer>
<button className="btn btn-primary" onClick={handleReturnCurrentPosition}>Return current position</button>
<SearchBarMap/>
</>
)
}
function SearchBarMap() {
return <>
</>
}

View File

@@ -0,0 +1,16 @@
import {useNavigate} from "react-router-dom";
export function NewClubPage() {
const navigate = useNavigate();
return <>
<h2>Page affiliation</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/affiliation")}>
&laquo; retour
</button>
<div>
<div className="row">
</div>
</div>
</>
}

View File

@@ -2,7 +2,7 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import imageCompression from "browser-image-compression";
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx";
export function addPhoto(event, formData, send) {
@@ -74,8 +74,7 @@ export function InformationForm({data}) {
type="email"/>
<OptionField name="genre" text="Genre" value={data.genre}
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<OptionField name="country" text="Pays" value={data.country}
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
<CountryList name="country" text="Pays" value={data.country}/>
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
inti_category={data.categorie}/>
<div className="row">
@@ -86,7 +85,11 @@ export function InformationForm({data}) {
MEMBRE: 'Membre',
PRESIDENT: 'Président',
TRESORIER: 'Trésorier',
SECRETAIRE: 'Secrétaire'
SECRETAIRE: 'Secrétaire',
VPRESIDENT: 'Vise-Président',
VTRESORIER: 'Vise-Trésorier',
VSECRETAIRE: 'Vise-Secrétaire',
MEMBREBUREAU: 'Membre bureau'
}}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>

View File

@@ -3,7 +3,7 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {addPhoto} from "../../admin/member/InformationForm.jsx";
export function InformationForm({data}) {
@@ -52,8 +52,7 @@ export function InformationForm({data}) {
type="email"/>
<OptionField name="genre" text="Genre" value={data.genre}
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<OptionField name="country" text="Pays" value={data.country}
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
<CountryList name="country" text="Pays" value={data.country}/>
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
inti_category={data.categorie}/>
<OptionField name="role" text="Rôle" value={data.role}
@@ -61,7 +60,11 @@ export function InformationForm({data}) {
MEMBRE: 'Membre',
PRESIDENT: 'Président',
TRESORIER: 'Trésorier',
SECRETAIRE: 'Secrétaire'
SECRETAIRE: 'Secrétaire',
VPRESIDENT: 'Vise-Président',
VTRESORIER: 'Vise-Trésorier',
VSECRETAIRE: 'Vise-Secrétaire',
MEMBREBUREAU: 'Membre bureau'
}} disabled={true}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>