wip: addr field

This commit is contained in:
Thibaut Valentin 2024-07-12 16:37:04 +02:00
parent 76d7a28678
commit 6c4b01590d
6 changed files with 269 additions and 12 deletions

View File

@ -5,6 +5,7 @@ import fr.titionfire.ffsaf.domain.service.ClubService;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel; import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.rest.data.SimpleClub; import fr.titionfire.ffsaf.rest.data.SimpleClub;
import fr.titionfire.ffsaf.rest.from.FullClubForm; import fr.titionfire.ffsaf.rest.from.FullClubForm;
import fr.titionfire.ffsaf.utils.Contact;
import fr.titionfire.ffsaf.utils.GroupeUtils; import fr.titionfire.ffsaf.utils.GroupeUtils;
import fr.titionfire.ffsaf.utils.PageResult; import fr.titionfire.ffsaf.utils.PageResult;
import fr.titionfire.ffsaf.utils.Utils; import fr.titionfire.ffsaf.utils.Utils;
@ -76,7 +77,9 @@ public class ClubEndpoints {
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"}) @RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleClub> getById(@PathParam("id") long id) { public Uni<SimpleClub> getById(@PathParam("id") long id) {
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel); return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel).invoke(m -> {
m.setContactMap(Contact.toSite());
});
} }
@PUT @PUT

View File

@ -8,6 +8,7 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.ToString; import lombok.ToString;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Data @Data
@ -29,6 +30,7 @@ public class SimpleClub {
private String SIRET; private String SIRET;
private String no_affiliation; private String no_affiliation;
private boolean international; private boolean international;
private HashMap<String, String> contactMap = null;
public static SimpleClub fromModel(ClubModel model) { public static SimpleClub fromModel(ClubModel model) {
if (model == null) if (model == null)

View File

@ -2,6 +2,9 @@ package fr.titionfire.ffsaf.utils;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import javax.naming.ldap.HasControls;
import java.util.HashMap;
@RegisterForReflection @RegisterForReflection
public enum Contact { public enum Contact {
COURRIEL("Courriel"), COURRIEL("Courriel"),
@ -20,8 +23,11 @@ public enum Contact {
this.name = name; this.name = name;
} }
@Override public static HashMap<String, String> toSite() {
public String toString() { HashMap<String, String> map = new HashMap<>();
return name; for (Contact contact : Contact.values()) {
map.put(contact.toString(), contact.name);
}
return map;
} }
} }

View File

@ -0,0 +1,83 @@
import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "./AxiosError.jsx";
function SimpleReducer(datas, action) {
switch (action.type) {
case 'ADD':
return [
...datas,
action.payload
]
case 'REMOVE':
return datas.filter(data => data.id !== action.payload)
case 'UPDATE_OR_ADD':
const index = datas.findIndex(data => data.id === action.payload.id)
if (index === -1) {
return [
...datas,
action.payload
]
} else {
datas[index] = action.payload
return [...datas]
}
default:
throw new Error()
}
}
export function ListEditorTest() {
const [html, dispatch] = ListEditor(ListHTML)
useEffect(() => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: 1, content: "data in"}})
}, []);
return html
}
export function ListEditor(ListItem) {
const [modal, setModal] = useState({id: -1})
const [state, dispatch] = useReducer(SimpleReducer, [])
const sendAffiliation = (e) => {
dispatch({type: 'UPDATE_OR_ADD', payload: e})
}
return [<>
<ul className="list-group">
{state.map((d, index) => {
return <div key={index} className={"list-group-item d-flex justify-content-between align-items-start"}>
<ListItem data={d}/>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#EditModal" onClick={_ => setModal(d)}>
<FontAwesomeIcon icon={faPen}/></button>
<button className="badge btn btn-danger rounded-pill"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
<FontAwesomeIcon icon={faTrashCan}/></button>
</div>
})}
</ul>
<div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<form onSubmit={e => sendAffiliation(e, dispatch)}>
<input name="id" value={modal.id} readOnly hidden/>
</form>
<ModalContent affiliation={modalAffiliation} dispatch={dispatch}/>
</div>
</div>
</div>
</>
, dispatch]
}
function ListHTML({
data
}) {
return <div className="me-auto">{data.content}</div>
}

View File

@ -4,9 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen} from "@fortawesome/free-solid-svg-icons"; import {faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {apiAxios, getSaison} from "../../../utils/Tools.js"; import {apiAxios, getSaison} from "../../../utils/Tools.js";
import {Input} from "../../../components/Input.jsx";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
function affiliationReducer(affiliation, action) { function affiliationReducer(affiliation, action) {

View File

@ -9,9 +9,38 @@ import {AffiliationCard} from "./AffiliationCard.jsx";
import {CheckField, CountryList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {CheckField, CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {MapContainer, Marker, Popup, TileLayer, useMap} from 'react-leaflet' import {MapContainer, Marker, Popup, TileLayer, useMap} from 'react-leaflet'
import {ListEditorTest} from "../../../components/ListEditor.jsx";
import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
function SimpleReducer(datas, action) {
switch (action.type) {
case 'ADD':
return [
...datas,
action.payload
]
case 'REMOVE':
return datas.filter(data => data.id !== action.payload)
case 'UPDATE_OR_ADD':
const index = datas.findIndex(data => data.id === action.payload.id)
if (index === -1) {
return [
...datas,
action.payload
]
} else {
datas[index] = action.payload
return [...datas]
}
default:
throw new Error()
}
}
export function ClubPage() { export function ClubPage() {
const {id} = useParams() const {id} = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
@ -41,15 +70,19 @@ export function ClubPage() {
? <div> ? <div>
<div className="row"> <div className="row">
<div className="col-lg-8"> <div className="col-lg-8">
<LoadingProvider><InformationForm data={data}/></LoadingProvider> <LoadingProvider>
<InformationForm data={data}/>
</LoadingProvider>
</div> </div>
<div className="col-lg-4"> <div className="col-lg-4">
<LoadingProvider><AffiliationCard clubData={data}/></LoadingProvider> <LoadingProvider><AffiliationCard clubData={data}/></LoadingProvider>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}> <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 className="btn btn-danger btn-sm" data-bs-toggle="modal"
data-bs-target="#confirm-delete">Supprimer le compte
</button> </button>
</div> </div>
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?" <ConfirmDialog title="Supprimer le compte"
message="Êtes-vous sûr de vouloir supprimer ce compte ?"
onConfirm={handleRm}/> onConfirm={handleRm}/>
</div> </div>
</div> </div>
@ -83,18 +116,150 @@ function InformationForm({data}) {
<div className="form-text" id="url_photo">Laissez vide pour ne rien changer.</div> <div className="form-text" id="url_photo">Laissez vide pour ne rien changer.</div>
</div> </div>
<TextField name="contact" text="Contact" value={data.contact}/> <ContactEditor data={data}/>
<LocationEditor data={data}/>
<TextField name="training_location" text="Lieux d'entrainement" value={data.training_location}/> <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="training_day_time" text="Horaire d'entrainement" value={data.training_day_time}/>
<TextField name="contact_intern" text="Contact" value={"contact_intern"}/> <TextField name="contact_intern" text="Contact" value={"contact_intern"}/>
<CheckField name="international" text="Club international" value={data.international}/> <CheckField name="international" text="Club international" value={data.international}/>
<MainMap/> <MainMap/>
</div> </div>
</div>; </div>;
} }
// https://annuaire-entreprises.data.gouv.fr/entreprise/la-mesnie-des-chevaliers-de-st-georges-et-de-st-michel-500213731
export function LocationEditor({data}) {
const [modal, setModal] = useState({id: -1})
const [state, dispatch] = useReducer(SimpleReducer, [])
useEffect(() => {
JSON.parse(data.training_location).forEach((d, index) => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}})
})
}, [data.training_location]);
const sendAffiliation = (e) => {
dispatch({type: 'UPDATE_OR_ADD', payload: e})
}
return <>
<ul className="list-group">
{state.map((d, index) => {
return <div key={index} className={"list-group-item d-flex justify-content-between align-items-start"}>
<div className="me-auto">{d.data.text}</div>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#EditModal" onClick={_ => setModal(d)}>
<FontAwesomeIcon icon={faPen}/></button>
<button className="badge btn btn-danger rounded-pill"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
<FontAwesomeIcon icon={faTrashCan}/></button>
</div>
})}
</ul>
<div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h1 className="modal-title fs-5" id="EditModalLabel">Edition de l'adresse</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">
<form onSubmit={e => sendAffiliation(e, dispatch)}>
<input name="id" value={modal.id} readOnly hidden/>
</form>
<Autoc/>
</div>
</div>
</div>
</div>
</div>
</>
}
function Autoc() {
const [location, setLocation] = useState("9 rue Gracchus")
const {
data,
error,
refresh
} = useFetch(`https://api-adresse.data.gouv.fr/search/?q=${encodeURI(location)}&type=housenumber&autocomplete=1`)
useEffect(() => {
refresh(`https://api-adresse.data.gouv.fr/search/?q=${encodeURI(location)}&type=housenumber&autocomplete=1`)
}, [location]);
return <>
<div className="form-group">
<label htmlFor="input-datalist">Timezone</label>
<input role="combobox" aria-autocomplete="list" aria-expanded="false" autoComplete="off"
placeholder="Chercher une adresse..." aria-label="Recherche"
className="form-control" list="addr" value={location}
onChange={e => setLocation(e.target.value)}/>
<datalist id="addr">
{data && data.features.map((d, index) => {
return <option key={index}>{d.properties.label}</option>
})}
</datalist>
</div>
</>
}
export function ContactEditor({
data
}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
useEffect(() => {
for (const key in data.contact) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: key, data: data.contact[key]}})
}
}, [data.contact]);
return <>
<ul className="list-group">
{state.map((d, index) => {
if (d.data === undefined)
return;
return <div key={index} className={"list-group-item d-flex justify-content-between align-items-start"}>
<div className="input-group">
<select className="form-select" aria-label="type" defaultValue={d.id}
onChange={(e) => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: undefined}})
dispatch({type: 'UPDATE_OR_ADD', payload: {id: e.target.value, data: d.data}})
}}>
{Object.keys(data.contactMap).map((key, _) => {
let b = false;
for (let s of state) {
if (s.id === key && s.data !== undefined) b = true;
}
return (<option key={key} value={key} disabled={b}>{data.contactMap[key]}</option>)
})}
</select>
<input type="text" className="form-control" defaultValue={d.data} required
onChange={(e) => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: e.target.value}})
}}/>
<button className="btn btn-danger" type="button"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}><FontAwesomeIcon
icon={faTrashCan}/>
</button>
</div>
</div>
})}
</ul>
</>
}
// 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] const position = [51.505, -0.09]
function MainMap() { function MainMap() {
function handleReturnCurrentPosition() { function handleReturnCurrentPosition() {
console.log("I have clicked return button!!"); console.log("I have clicked return button!!");
@ -105,7 +270,7 @@ function MainMap() {
return ( return (
<> <>
<MapContainer center={position} zoom={13} scrollWheelZoom={false} style={{height: "30em", width: "50em"}}> <MapContainer center={position} zoom={13} scrollWheelZoom={false} style={{height: "30em", width: "50em"}}>
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"