feat: start affiliation system
This commit is contained in:
parent
2a59c22db6
commit
40427b8cfb
4
pom.xml
4
pom.xml
@ -56,6 +56,10 @@
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
|
||||
28
src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java
Normal file
28
src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java
Normal file
@ -0,0 +1,28 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.client.SirenService;
|
||||
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
|
||||
@Path("api/asso")
|
||||
public class AssoEndpoints {
|
||||
|
||||
@RestClient
|
||||
SirenService sirenService;
|
||||
|
||||
@GET
|
||||
@Path("siren/{siren}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Uni<UniteLegaleRoot> getInfoSiren(@PathParam("siren") String siren) {
|
||||
return sirenService.get_unite(siren).onFailure().transform(throwable -> {
|
||||
if (throwable instanceof WebApplicationException exception){
|
||||
if (exception.getResponse().getStatus() == 400)
|
||||
return new BadRequestException("Not found");
|
||||
}
|
||||
return throwable;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package fr.titionfire.ffsaf.rest.client;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
@Path("/")
|
||||
@RegisterRestClient
|
||||
@ClientHeaderParam(name = "X-Client-Secret", value = "${siren-api.key}")
|
||||
public interface SirenService {
|
||||
|
||||
@GET
|
||||
@Path("/v3/unites_legales/{SIREN}")
|
||||
Uni<UniteLegaleRoot> get_unite(@PathParam("SIREN") String siren);
|
||||
}
|
||||
107
src/main/java/fr/titionfire/ffsaf/rest/data/UniteLegaleRoot.java
Normal file
107
src/main/java/fr/titionfire/ffsaf/rest/data/UniteLegaleRoot.java
Normal file
@ -0,0 +1,107 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UniteLegaleRoot {
|
||||
public UniteLegale unite_legale;
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public static class UniteLegale {
|
||||
public String activite_principale;
|
||||
public Object annee_categorie_entreprise;
|
||||
public Object annee_effectifs;
|
||||
public Object caractere_employeur;
|
||||
public Object categorie_entreprise;
|
||||
public String categorie_juridique;
|
||||
public String date_creation;
|
||||
public String date_debut;
|
||||
public Date date_dernier_traitement;
|
||||
public String denomination;
|
||||
public Object denomination_usuelle_1;
|
||||
public Object denomination_usuelle_2;
|
||||
public Object denomination_usuelle_3;
|
||||
public String economie_sociale_solidaire;
|
||||
public Etablissement etablissement_siege;
|
||||
public ArrayList<Etablissement> etablissements;
|
||||
public String etat_administratif;
|
||||
public String identifiant_association;
|
||||
public String nic_siege;
|
||||
public Object nom;
|
||||
public Object nom_usage;
|
||||
public int nombre_periodes;
|
||||
public String nomenclature_activite_principale;
|
||||
public Object prenom_1;
|
||||
public Object prenom_2;
|
||||
public Object prenom_3;
|
||||
public Object prenom_4;
|
||||
public Object prenom_usuel;
|
||||
public Object pseudonyme;
|
||||
public Object sexe;
|
||||
public Object sigle;
|
||||
public String siren;
|
||||
public String societe_mission;
|
||||
public String statut_diffusion;
|
||||
public Object tranche_effectifs;
|
||||
public Object unite_purgee;
|
||||
}
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public static class Etablissement {
|
||||
private String activite_principale;
|
||||
private Object activite_principale_registre_metiers;
|
||||
private Object annee_effectifs;
|
||||
private String caractere_employeur;
|
||||
private Object code_cedex;
|
||||
private Object code_cedex_2;
|
||||
private String code_commune;
|
||||
private Object code_commune_2;
|
||||
private Object code_pays_etranger;
|
||||
private Object code_pays_etranger_2;
|
||||
private String code_postal;
|
||||
private Object code_postal_2;
|
||||
private Object complement_adresse;
|
||||
private Object complement_adresse2;
|
||||
private String date_creation;
|
||||
private String date_debut;
|
||||
private Date date_dernier_traitement;
|
||||
private Object denomination_usuelle;
|
||||
private Object distribution_speciale;
|
||||
private Object distribution_speciale_2;
|
||||
private Object enseigne_1;
|
||||
private Object enseigne_2;
|
||||
private Object enseigne_3;
|
||||
private boolean etablissement_siege;
|
||||
private String etat_administratif;
|
||||
private Object indice_repetition;
|
||||
private Object indice_repetition_2;
|
||||
private Object libelle_cedex;
|
||||
private Object libelle_cedex_2;
|
||||
private String libelle_commune;
|
||||
private Object libelle_commune_2;
|
||||
private Object libelle_commune_etranger;
|
||||
private Object libelle_commune_etranger_2;
|
||||
private Object libelle_pays_etranger;
|
||||
private Object libelle_pays_etranger_2;
|
||||
private String libelle_voie;
|
||||
private Object libelle_voie_2;
|
||||
private String nic;
|
||||
private int nombre_periodes;
|
||||
private String nomenclature_activite_principale;
|
||||
private String numero_voie;
|
||||
private Object numero_voie_2;
|
||||
private String siren;
|
||||
private String siret;
|
||||
private String statut_diffusion;
|
||||
private Object tranche_effectifs;
|
||||
private String type_voie;
|
||||
private Object type_voie_2;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package fr.titionfire.ffsaf.rest.from;
|
||||
|
||||
import jakarta.ws.rs.FormParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jboss.resteasy.reactive.PartType;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class AffiliationRequestForm {
|
||||
@FormParam("name")
|
||||
private String name = null;
|
||||
|
||||
@FormParam("siren")
|
||||
private String siren = null;
|
||||
|
||||
@FormParam("rna")
|
||||
private String rna = null;
|
||||
|
||||
@FormParam("adresse")
|
||||
private String adresse = null;
|
||||
|
||||
@FormParam("status")
|
||||
@PartType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
private byte[] status = new byte[0];
|
||||
|
||||
@FormParam("logo")
|
||||
@PartType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
private byte[] logo = new byte[0];
|
||||
}
|
||||
@ -34,6 +34,9 @@ database.port=3306
|
||||
database.user=root
|
||||
database.pass=
|
||||
|
||||
siren-api.key=siren-ap
|
||||
quarkus.rest-client."fr.titionfire.ffsaf.rest.client.SirenService".url=https://data.siren-api.fr/
|
||||
|
||||
#Login
|
||||
quarkus.oidc.token-state-manager.split-tokens=true
|
||||
quarkus.oidc.token.refresh-expired=true
|
||||
|
||||
@ -11,6 +11,7 @@ import {ToastContainer} from "react-toastify";
|
||||
import './App.css'
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
|
||||
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -31,6 +32,19 @@ const router = createBrowserRouter([
|
||||
path: 'club',
|
||||
element: <ClubRoot/>,
|
||||
children: getClubChildren()
|
||||
},
|
||||
{
|
||||
path: 'affiliation',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <DemandeAff/>
|
||||
},
|
||||
{
|
||||
path: 'ok',
|
||||
element: <DemandeAffOk/>
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -24,6 +24,7 @@ export function Nav() {
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
|
||||
<ClubMenu/>
|
||||
<AdminMenu/>
|
||||
<AffiliationMenu/>
|
||||
<LoginMenu/>
|
||||
</ul>
|
||||
</div>
|
||||
@ -32,6 +33,15 @@ export function Nav() {
|
||||
</nav>
|
||||
}
|
||||
|
||||
|
||||
function AffiliationMenu() {
|
||||
const {is_authenticated} = useAuth()
|
||||
|
||||
if (is_authenticated)
|
||||
return <></>
|
||||
return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">Demande d'affiliation</NavLink></li>
|
||||
}
|
||||
|
||||
function ClubMenu() {
|
||||
const {is_authenticated, userinfo} = useAuth()
|
||||
|
||||
|
||||
187
src/main/webapp/src/pages/DemandeAff.jsx
Normal file
187
src/main/webapp/src/pages/DemandeAff.jsx
Normal file
@ -0,0 +1,187 @@
|
||||
import {useState} from "react";
|
||||
import {apiAxios} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
function reconstruireAdresse(infos) {
|
||||
let adresseReconstruite = "";
|
||||
adresseReconstruite += infos.numero_voie + ' ' + infos.type_voie + ' ';
|
||||
adresseReconstruite += infos.libelle_voie + ', ';
|
||||
adresseReconstruite += infos.code_postal + ' ' + infos.libelle_commune + ', ';
|
||||
|
||||
if (infos.complement_adresse) {
|
||||
adresseReconstruite += infos.complement_adresse + ', ';
|
||||
}
|
||||
if (infos.code_cedex && infos.libelle_cedex) {
|
||||
adresseReconstruite += 'Cedex ' + infos.code_cedex + ' - ' + infos.libelle_cedex;
|
||||
}
|
||||
|
||||
if (adresseReconstruite.endsWith(', ')) {
|
||||
adresseReconstruite = adresseReconstruite.slice(0, -2);
|
||||
}
|
||||
|
||||
return adresseReconstruite;
|
||||
}
|
||||
|
||||
|
||||
export function DemandeAff() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const submit = (event) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.target)
|
||||
toast.promise(
|
||||
apiAxios.post(`asso/affiliation`, formData),
|
||||
{
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>Demande d'affiliation</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>
|
||||
|
||||
<div className="card mb-4">
|
||||
<form onSubmit={submit}>
|
||||
<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"/>
|
||||
|
||||
<div className="mb-3">
|
||||
<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 :
|
||||
<ul>
|
||||
<li>Disposer d’au moins trois membres licenciés, dont le président, le trésorier et le secrétaire</li>
|
||||
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button type="submit" className="btn btn-primary">Confirmer ma demande d'affiliation</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function AssoInfo() {
|
||||
const [denomination, setDenomination] = useState("")
|
||||
const [siren, setSiren] = useState("")
|
||||
const [rna, setRna] = useState("")
|
||||
const [rnaEnable, setRnaEnable] = useState(false)
|
||||
const [adresse, setAdresse] = useState("")
|
||||
|
||||
const fetchSiren = () => {
|
||||
toast.promise(
|
||||
apiAxios.get(`asso/siren/${siren}`),
|
||||
{
|
||||
pending: "Recherche de l'association en cours",
|
||||
success: "Association trouvée avec succès 🎉",
|
||||
error: "Échec de la recherche de l'association 😕"
|
||||
}
|
||||
).then(data => {
|
||||
const data2 = data.data.unite_legale
|
||||
setDenomination(data2.denomination)
|
||||
setRnaEnable(data2.identifiant_association === null)
|
||||
setRna(data2.identifiant_association ? data2.identifiant_association : "")
|
||||
setAdresse(reconstruireAdresse(data2.etablissement_siege))
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<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/>
|
||||
</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)}/>
|
||||
<button className="btn btn-outline-secondary" type="button" id="button-addon2" onClick={fetchSiren}>Rechercher</button>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<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="input-group mb-3">
|
||||
<span className="input-group-text" id="basic-addon1">RNA</span>
|
||||
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA" aria-describedby="basic-addon1"
|
||||
disabled={!rnaEnable} name="rna" value={rna} onChange={e => setRna(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="basic-addon1">Adresse*</span>
|
||||
<input type="text" className="form-control" placeholder="Adresse" aria-label="Adresse" aria-describedby="basic-addon1"
|
||||
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="status">Status*</label>
|
||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="logo">Logo*</label>
|
||||
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg" required/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</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 login et mot de passe provisoire pour accéder à votre espace FFSAF</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user