All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m4s
383 lines
17 KiB
JavaScript
383 lines
17 KiB
JavaScript
import {useEffect, useState} from "react";
|
||
import {apiAxios, errFormater, getSaison} from "../utils/Tools.js";
|
||
import {toast} from "react-toastify";
|
||
import {useLocation, useNavigate} from "react-router-dom";
|
||
|
||
const notUpperCase = ["de", "la", "le", "les", "des", "du", "d'", "l'", "sur", 'lieu', 'dit'];
|
||
|
||
function formatAdresse(data) {
|
||
const words = data.split(" ");
|
||
|
||
return words.map((word) => {
|
||
if (notUpperCase.includes(word.toLowerCase())) {
|
||
return word.toLowerCase();
|
||
}
|
||
return word[0].toUpperCase() + word.substring(1).toLowerCase();
|
||
}).join(" ");
|
||
}
|
||
|
||
function reconstruireAdresse2(infos) {
|
||
let adresseReconstruite = "";
|
||
|
||
if (infos?.complement)
|
||
adresseReconstruite += formatAdresse(infos.complement) + ', ';
|
||
|
||
adresseReconstruite += formatAdresse(infos.voie) + ', ';
|
||
adresseReconstruite += infos.code_postal + ' ' + infos.commune.nom + ', ';
|
||
|
||
if (infos?.pays)
|
||
adresseReconstruite += formatAdresse(infos.pays) + ', ';
|
||
|
||
if (adresseReconstruite.endsWith(', ')) {
|
||
adresseReconstruite = adresseReconstruite.slice(0, -2);
|
||
}
|
||
|
||
return adresseReconstruite;
|
||
}
|
||
|
||
function getSaisonToAff(currentDate = new Date()) {
|
||
if (currentDate.getMonth() >= 7) { //aout et plus
|
||
return currentDate.getFullYear()
|
||
} else {
|
||
return currentDate.getFullYear() - 1
|
||
}
|
||
}
|
||
|
||
export function DemandeAff() {
|
||
const {hash} = useLocation();
|
||
const navigate = useNavigate();
|
||
const [initData, setInitData] = useState(null)
|
||
const [needFile, setNeedFile] = useState(true)
|
||
|
||
useEffect(() => {
|
||
if (hash.startsWith("#d")) {
|
||
const data = JSON.parse(decodeURI(hash.substring(2)));
|
||
setInitData(data)
|
||
setNeedFile(false)
|
||
} else if (hash.startsWith("#e")) {
|
||
apiAxios.get(`/affiliation/request/${hash.substring(2)}`).then(data => {
|
||
for (let i = 0; i < data.data.members.length; i++) {
|
||
if (data.data.members[i].licence === -1)
|
||
data.data.members[i].licence = ""
|
||
}
|
||
setInitData(data.data)
|
||
setNeedFile(false)
|
||
}).catch(_ => {
|
||
setInitData({})
|
||
})
|
||
} else {
|
||
setInitData({})
|
||
}
|
||
}, []);
|
||
|
||
const submit = (event) => {
|
||
event.preventDefault()
|
||
const formData = new FormData(event.target)
|
||
formData.append("m1_role", event.target.m1_role?.value)
|
||
formData.append("state_id", event.target.state_id?.value)
|
||
|
||
let error = false;
|
||
for (let i = 1; i <= 3; i++) {
|
||
if (event.target[`m${i}_role`]?.value === "0") {
|
||
toast.error(`Le rôle du membre ${i} est obligatoire`)
|
||
error = true;
|
||
}
|
||
}
|
||
if (error) {
|
||
return;
|
||
}
|
||
|
||
if (event.nativeEvent.submitter.value === "undo") {
|
||
toast.promise(
|
||
apiAxios.delete(`/affiliation/request/${initData.id}`),
|
||
{
|
||
pending: "Annulation de la demande d'affiliation en cours",
|
||
success: "Demande d'affiliation annulée avec succès 🎉",
|
||
error: {
|
||
render({data}) {
|
||
return errFormater(data, "Échec de l'annulation de la demande d'affiliation")
|
||
}
|
||
}
|
||
}
|
||
).then(_ => {
|
||
navigate("/club/me")
|
||
})
|
||
} else if (event.nativeEvent.submitter.value === "edit") {
|
||
formData.append("id", initData.id)
|
||
|
||
toast.promise(
|
||
apiAxios.put(`/affiliation/request/edit`, formData),
|
||
{
|
||
pending: "Enregistrement des modifications en cours",
|
||
success: "Modifications enregistrées avec succès 🎉",
|
||
error: {
|
||
render({data}) {
|
||
return errFormater(data, "Échec de l'enregistrement des modifications")
|
||
}
|
||
}
|
||
}
|
||
).then(_ => {
|
||
navigate("/club/me")
|
||
})
|
||
} else {
|
||
formData.append("id", -1)
|
||
|
||
toast.promise(
|
||
apiAxios.post(`/affiliation/request`, formData, {headers: {'Accept': '*/*'}}),
|
||
{
|
||
pending: "Enregistrement de la demande d'affiliation en cours",
|
||
success: "Demande d'affiliation enregistrée avec succès 🎉",
|
||
error: {
|
||
render({data}) {
|
||
return errFormater(data, "Échec de la demande d'affiliation")
|
||
}
|
||
}
|
||
}
|
||
).then(_ => {
|
||
navigate("/affiliation/ok")
|
||
})
|
||
}
|
||
}
|
||
|
||
return <div>
|
||
<h1>Demande d'affiliation {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}</h1>
|
||
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année
|
||
suivante.</p>
|
||
Pour s’affilier, une association sportive doit réunir les conditions suivantes :
|
||
<ul>
|
||
<li>Avoir son siège social en France ou Principauté de Monaco</li>
|
||
<li>Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport</li>
|
||
<li>Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération</li>
|
||
<li>Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la
|
||
Fédération
|
||
</li>
|
||
<li>Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute
|
||
discrimination
|
||
</li>
|
||
<li>Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la
|
||
Fédération
|
||
</li>
|
||
</ul>
|
||
|
||
{initData && <div className="card mb-4">
|
||
<form onSubmit={submit}>
|
||
<div className="card-body">
|
||
<h4>L'association</h4>
|
||
<AssoInfo initData={initData} needFile={needFile}/>
|
||
<h4>Membre n°1</h4>
|
||
<MembreInfo role="m1" initData={initData.members?.at(0) || {}}/>
|
||
<h4 style={{marginTop: '1em'}}>Membre n°2</h4>
|
||
<MembreInfo role="m2" initData={initData.members?.at(1) || {}}/>
|
||
<h4 style={{marginTop: '1em'}}>Membre n°3</h4>
|
||
<MembreInfo role="m3" initData={initData.members?.at(2) || {}}/>
|
||
|
||
<div className="mb-3" style={{marginTop: '1em'}}>
|
||
{!initData.id && <p>Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour
|
||
accéder à votre espace FFSAF</p>}
|
||
Notez que pour finaliser votre affiliation, il vous faudra :
|
||
<ul>
|
||
<li>Disposer d’au moins trois membres licenciés, dont le président</li>
|
||
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{!initData.id ?
|
||
<div className="row">
|
||
<div className="d-grid gap-2 d-md-flex justify-content-md-center">
|
||
<button type="submit" value="new" className="btn btn-primary">Confirmer ma demande d'affiliation
|
||
</button>
|
||
</div>
|
||
</div> :
|
||
<div className="row">
|
||
<div className="d-grid gap-2 d-md-flex justify-content-md-center col">
|
||
<button type="submit" value="undo" className="btn btn-danger">Annuler ma demande
|
||
</button>
|
||
</div>
|
||
<div className="d-grid gap-2 d-md-flex justify-content-md-center col">
|
||
<button type="submit" value="edit" className="btn btn-primary">Enregistrer les modifications
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</form>
|
||
</div>}
|
||
</div>
|
||
}
|
||
|
||
function AssoInfo({initData, needFile}) {
|
||
const [denomination, setDenomination] = useState("")
|
||
const [stateId, setStateId] = useState(initData.stateId ? String(initData.stateId) : (initData.state_id ? String(initData.state_id) : ""))
|
||
const [adresse, setAdresse] = useState(initData.address ? initData.address : "")
|
||
const [contact, setContact] = useState(initData.contact ? initData.contact : "")
|
||
|
||
const fetchStateId = () => {
|
||
const regex = /^(?:\d{14}|W\d{9})$/;
|
||
let sid = stateId;
|
||
if (!regex.test(stateId)) {
|
||
toast.error("Le format du SIRET/RNA est invalide");
|
||
return;
|
||
}else{
|
||
if (stateId[0] !== 'W')
|
||
sid = stateId.substring(0, 9); // Pour les SIRET, on ne garde que les 9 premiers chiffres (SIREN)
|
||
}
|
||
|
||
toast.promise(
|
||
apiAxios.get(`asso/state_id/${sid}`),
|
||
{
|
||
pending: "Recherche de l'association en cours",
|
||
success: "Association trouvée avec succès 🎉",
|
||
error: {
|
||
render({data}) {
|
||
return errFormater(data, "Échec de la recherche de l'association")
|
||
}
|
||
}
|
||
}
|
||
).then(data => {
|
||
const data2 = data.data
|
||
setDenomination(data2.identite.nom)
|
||
if (!initData.saison || adresse === "")
|
||
setAdresse(reconstruireAdresse2(data2.coordonnees.adresse_gestion))
|
||
})
|
||
}
|
||
return <>
|
||
<input name="saison" value={getSaisonToAff()} readOnly hidden/>
|
||
|
||
<div className="input-group mb-3">
|
||
<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 defaultValue={initData.name ? initData.name : ""}/>
|
||
</div>
|
||
|
||
<div className="input-group mb-3">
|
||
<span className="input-group-text">N° SIRET ou RNA*</span>
|
||
<input type="text" className="form-control" placeholder="N° SIRET ou RNA*" name="state_id" required value={stateId} disabled={!needFile}
|
||
onChange={e => setStateId(e.target.value)}/>
|
||
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
|
||
onClick={fetchStateId} hidden={false}>Rechercher
|
||
</button>
|
||
</div>
|
||
|
||
<div className="input-group mb-3" hidden={false}>
|
||
<span className="input-group-text" id="basic-addon1">Dénomination</span>
|
||
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter"
|
||
aria-label="Dénomination"
|
||
aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
|
||
</div>
|
||
|
||
<div className="mb-3">
|
||
<div className="input-group">
|
||
<span className="input-group-text" id="basic-addon1">Adresse administrative*</span>
|
||
<input type="text" className="form-control" placeholder="Adresse administrative" aria-label="Adresse administrative"
|
||
aria-describedby="basic-addon1"
|
||
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
|
||
</div>
|
||
<div className="form-text" id="adresse">Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux
|
||
d'entrainement
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-3">
|
||
<div className="input-group">
|
||
<span className="input-group-text" id="basic-addon1">Contact administratif*</span>
|
||
<input type="email" className="form-control" placeholder="Contact administratif" aria-label="Contact administratif"
|
||
aria-describedby="basic-addon1"
|
||
required value={contact} name="contact" onChange={e => setContact(e.target.value)}/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-3">
|
||
<div className="input-group">
|
||
<label className="input-group-text" htmlFor="logo">Blason{needFile && "*"}</label>
|
||
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg"
|
||
required={needFile}/>
|
||
</div>
|
||
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette
|
||
demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)</div>}
|
||
</div>
|
||
|
||
<div className="mb-3">
|
||
<div className="input-group">
|
||
<label className="input-group-text" htmlFor="status">Statue{needFile && "*"}</label>
|
||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/>
|
||
</div>
|
||
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette
|
||
demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)</div>}
|
||
</div>
|
||
</>;
|
||
}
|
||
|
||
function MembreInfo({role, initData}) {
|
||
const [switchOn, setSwitchOn] = useState(!!initData.licence);
|
||
|
||
return <>
|
||
<div className="input-group mb-3">
|
||
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôles</label>
|
||
<select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)}
|
||
disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required>
|
||
<option value="0">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={initData.lname ? initData.lname : ""} 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={initData.fname ? initData.fname : ""} 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={initData.email ? initData.email : ""} required/>
|
||
<label htmlFor="floatingInput">Email*</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
{switchOn &&
|
||
<div className="col-sm-3">
|
||
<div className="form-floating">
|
||
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
|
||
name={role + "_licence"} defaultValue={initData.licence ? Number(initData.licence) : ""} required/>
|
||
<label htmlFor="floatingInput">N° Licence</label>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
</>
|
||
}
|
||
|
||
export function DemandeAffOk() {
|
||
return (
|
||
<div>
|
||
<h1 className="text-green-800 text-4xl">Demande d'affiliation envoyée avec succès</h1>
|
||
<p>Une fois votre demande validée, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre
|
||
espace FFSAF</p>
|
||
</div>
|
||
);
|
||
}
|