feat: club end main

This commit is contained in:
Thibaut Valentin 2024-07-14 13:24:02 +02:00
parent e1a8c90f3e
commit 6a21bd4735
12 changed files with 365 additions and 250 deletions

View File

@ -28,8 +28,6 @@ public class ClubModel {
String country;
String shieldURL;
//@Enumerated(EnumType.STRING)
@ElementCollection
@CollectionTable(name = "club_contact_mapping",
@ -37,15 +35,19 @@ public class ClubModel {
@MapKeyColumn(name = "contact_type")
Map<Contact, String> contact;
@Lob
@Column(length=4096)
String training_location;
@Lob
@Column(length=4096)
String training_day_time;
String contact_intern;
String RNA;
String SIRET;
Long SIRET;
String no_affiliation;

View File

@ -18,13 +18,12 @@ public class ClubEntity {
private String name;
private String clubId;
private String country;
private String shieldURL;
private Map<Contact, String> contact;
private String training_location;
private String training_day_time;
private String contact_intern;
private String RNA;
private String SIRET;
private Long SIRET;
private String no_affiliation;
private boolean international;
@ -38,7 +37,6 @@ public class ClubEntity {
.name(model.getName())
.clubId(model.getClubId())
.country(model.getCountry())
.shieldURL(model.getShieldURL())
.contact(model.getContact())
.training_location(model.getTraining_location())
.training_day_time(model.getTraining_day_time())

View File

@ -1,9 +1,14 @@
package fr.titionfire.ffsaf.domain.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.net2.request.SReqClub;
import fr.titionfire.ffsaf.rest.from.FullClubForm;
import fr.titionfire.ffsaf.utils.Contact;
import fr.titionfire.ffsaf.utils.PageResult;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
@ -19,8 +24,11 @@ import jakarta.ws.rs.BadRequestException;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@WithSession
@ApplicationScoped
public class ClubService {
@ -28,13 +36,18 @@ public class ClubService {
@Inject
ClubRepository repository;
@Inject
ServerCustom serverCustom;
public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
return VertxContextSupport.subscribeAndAwait(
() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
}
public Collection<SimpleClubModel> findAllClub() throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(
() -> repository.findAll().list().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList())));
() -> repository.findAll().list()
.map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList())));
}
public Uni<List<ClubModel>> getAll() {
@ -88,15 +101,32 @@ public class ClubService {
}
public Uni<String> update(long id, FullClubForm input) {
/*return repository.findById(id)
.onItem().transformToUni(m -> {
return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()))
.onItem().transformToUni(Unchecked.function(m -> {
TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
};
m.setName(input.getName());
m.setCountry(input.getCountry());
m.setNo_affiliation(input.getNo_affiliation());
m.setShieldURL(input.getShieldURL());
if (!input.isInternational()) {
m.setTraining_location(input.getTraining_location());
m.setTraining_day_time(input.getTraining_day_time());
m.setContact_intern(input.getContact_intern());
m.setRNA(input.getRna());
m.setSIRET(input.getSiret());
try {
m.setContact(MAPPER.readValue(input.getContact(), typeRef));
} catch (JsonProcessingException e) {
throw new BadRequestException();
}
}
return Panache.withTransaction(() -> repository.persist(m));
});*/
return Uni.createFrom().nullItem();
}))
.invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
SimpleClubModel.fromModel(membreModel)))
.map(__ -> "OK");
}
public Uni<Long> add(FullClubForm input) {

View File

@ -23,7 +23,7 @@ public class SimpleClubModel {
if (model == null)
return null;
return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), model.getShieldURL(),
model.getNo_affiliation());
return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(),
"/api/club/" + model.getClubId() + "/logo", model.getNo_affiliation());
}
}

View File

@ -21,13 +21,12 @@ public class SimpleClub {
private String clubId;
private String name;
private String country;
private String shieldURL;
private Map<Contact, String> contact;
private String training_location;
private String training_day_time;
private String contact_intern;
private String RNA;
private String SIRET;
private Long SIRET;
private String no_affiliation;
private boolean international;
private HashMap<String, String> contactMap = null;
@ -41,7 +40,6 @@ public class SimpleClub {
.clubId(model.getClubId())
.name(model.getName())
.country(model.getCountry())
.shieldURL(model.getShieldURL())
.contact(model.getContact())
.training_location(model.getTraining_location())
.training_day_time(model.getTraining_day_time())

View File

@ -3,8 +3,10 @@ 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;
@ToString
@Getter
public class FullClubForm {
@FormParam("id")
@ -13,6 +15,30 @@ public class FullClubForm {
@FormParam("name")
private String name = null;
@FormParam("country")
private String country = null;
@FormParam("contact")
private String contact = null;
@FormParam("training_location")
private String training_location = null;
@FormParam("training_day_time")
private String training_day_time = null;
@FormParam("contact_intern")
private String contact_intern = null;
@FormParam("rna")
private String rna = null;
@FormParam("siret")
private Long siret = null;
@FormParam("international")
private boolean international = false;
@FormParam("status")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] status = new byte[0];

View File

@ -54,7 +54,7 @@ public class Utils {
File dirFile = new File(media, dir);
if (!dirFile.exists())
if (dirFile.mkdirs())
if (!dirFile.mkdirs())
throw new IOException("Fail to create directory " + dir);
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));

View File

@ -0,0 +1,79 @@
import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
export function ContactEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
useEffect(() => {
for (const key in data.contact) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: key, data: data.contact[key]}})
}
}, [data.contact]);
useEffect(() => {
let out_data2 = {}
state.forEach(d => {
if (d.data !== undefined)
out_data2[d.id] = d.data
})
setOutData(out_data2)
}, [state]);
return <div className="row mb-3">
<input name="contact" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Contacts</span>
<ul className="list-group form-control">
{state.map((d, index) => {
if (d.data === undefined)
return;
return <div key={index} 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 className="d-grid gap-2 d-md-flex justify-content-md-end">
<button className="btn btn-success" type="button"
onClick={() => {
let id = null;
for (let key in data.contactMap) {
let b = false;
for (let s of state) {
if (s.id === key && s.data !== undefined) b = true;
}
if (!b) {
id = key
break
}
}
if (id !== null)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: id, data: ''}})
}}>
<FontAwesomeIcon icon={faAdd}/>
</button>
</div>
</ul>
</div>
}

View File

@ -0,0 +1,95 @@
import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
function timeNumberToSting(nbMin) {
return String(Math.floor(nbMin / 60)).padStart(2, '0') + ":" + String(nbMin % 60).padStart(2, '0')
}
function timeStringToNumber(time) {
let times = time.split(':');
return parseInt(times[0]) * 60 + parseInt(times[1]);
}
export function HoraireEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
useEffect(() => {
if (data.training_day_time === null)
return
JSON.parse(data.training_day_time).forEach((d, index) => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}})
})
}, [data.training_day_time]);
useEffect(() => {
setOutData(state.map(d => {
return {day: d.data.day, time_start: d.data.time_start, time_end: d.data.time_end}
}))
}, [state]);
const sortHoraire = (a, b) => {
if (a.data.day === b.data.day)
return a.data.time_start - b.data.time_start;
return a.data.day - b.data.day;
}
return <div className="row mb-3">
<input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Horaires d'entrainements</span>
<ul className="list-group form-control">
{state.map((d, index) => {
return <div key={index} className="input-group">
<select className="form-select" aria-label="type" value={d.data.day}
onChange={(e) => {
d.data.day = Number(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire})
}}>
<option value="0">Lundi</option>
<option value="1">Mardi</option>
<option value="2">Mercredi</option>
<option value="3">Jeudi</option>
<option value="4">Vendredi</option>
<option value="5">Samedi</option>
<option value="6">Dimanche</option>
</select>
<span className="input-group-text">de</span>
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_start)} required
onChange={(e) => {
d.data.time_start = timeStringToNumber(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire})
}}/>
<span className="input-group-text">à</span>
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_end)} required
onChange={(e) => {
d.data.time_end = timeStringToNumber(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire})
}}/>
<button className="btn btn-danger" type="button"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
<FontAwesomeIcon icon={faTrashCan}/>
</button>
</div>
})}
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button className="btn btn-success" type="button"
disabled={state.length >= 21}
onClick={() => {
let maxId = 0;
state.forEach((d) => {
if (d.id > maxId)
maxId = d.id;
})
dispatch({type: 'UPDATE_OR_ADD', payload: {id: maxId + 1, data: {day: 6, time_start: 0, time_end: 0}}})
}}>
<FontAwesomeIcon icon={faAdd}/>
</button>
</div>
</ul>
</div>
}

View File

@ -1,10 +1,10 @@
import {useEffect, useReducer, useRef, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {faAdd, faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import proj4 from "proj4";
import {useFetch} from "../../../hooks/useFetch.js";
import {useFetch} from "../../hooks/useFetch.js";
import {MapContainer, Marker, TileLayer} from "react-leaflet";
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
export function LocationEditor({data, setModal, sendData}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
@ -40,64 +40,46 @@ export function LocationEditor({data, setModal, sendData}) {
}))
}, [state]);
return <div className="row">
return <div className="row mb-3">
<input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Lieux d'entrainements</span>
<div className="input-group mb-3">
<ul className="list-group form-control">
{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={e => {
e.preventDefault();
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>
<ul className="list-group form-control">
{state.map((d, index) => {
return <div key={index} className="input-group">
<input type="text" className="form-control" value={d.data.text} required
onChange={(e) => {
}}/>
</div>
<button className="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#EditModal" onClick={e => {
e.preventDefault();
setModal(d);
}}>
<FontAwesomeIcon icon={faPen}/></button>
<button className="btn btn-danger"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
<FontAwesomeIcon icon={faTrashCan}/></button>
</div>
})}
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button className="btn btn-success" data-bs-toggle="modal"
disabled={state.length >= 10}
data-bs-target="#EditModal" onClick={e => {
e.preventDefault();
let maxId = 0;
state.forEach((d) => {
if (d.id > maxId)
maxId = d.id;
})
setModal({id: maxId + 1, data: {text: "", lat: undefined, lng: undefined}});
}}><FontAwesomeIcon icon={faAdd}/></button>
</div>
</ul>
</div>
}
export function LocationEditorModal({modal, sendData}) {
return <div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<form onSubmit={e => sendData.current(e)}>
<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">
<LocationEditorModalBody modal={modal}/>
</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>
</div>
</div>
</form>
</div>
</div>
}
proj4.defs("EPSG:9794", "+proj=lcc +lat_1=44 +lat_2=49 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");
function convertLambert93ToLatLng(x, y) {
const lambertPoint = proj4.toPoint([x, y]);
const wgs84Point = proj4("EPSG:9794", "EPSG:4326", lambertPoint);
return {lat: wgs84Point.y, lng: wgs84Point.x};
}
function LocationEditorModalBody({modal}) {
const [location, setLocation] = useState("")
const [locationObj, setLocationObj] = useState({text: "", lng: undefined, lat: undefined})
const [mapPosition, setMapPosition] = useState([46.652195, 2.430226])
@ -107,6 +89,7 @@ function LocationEditorModalBody({modal}) {
useEffect(() => {
if (modal.data !== undefined) {
setLocation(modal.data.text)
setLocationObj(modal.data)
}
}, [modal])
@ -143,34 +126,59 @@ function LocationEditorModalBody({modal}) {
return () => clearTimeout(delayDebounceFn)
}, [locationObj, modal])
return <div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<form onSubmit={e => sendData.current(e)}>
<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">
<input name="id" value={modal.id} readOnly hidden/>
<input name="loc_text" value={locationObj.text} readOnly hidden/>
<input name="loc_lat" value={locationObj.lat ? locationObj.lat : -142} readOnly hidden/>
<input name="loc_lng" value={locationObj.lng ? locationObj.lng : -142} readOnly hidden/>
<div className="row">
<div className="input-group mb-3">
<label className="input-group-text">Adresse</label>
<input className="form-control" aria-autocomplete="list" aria-expanded="true" autoComplete="true"
placeholder="Chercher une adresse..." aria-label="Recherche" list="addr" value={location}
onChange={e => setLocation(e.target.value)}/>
<datalist id="addr">
{data?.features && data.features.map((d, index) => {
return <option key={index}>{d.properties.label}</option>
})}
</datalist>
</div>
</div>
return <>
<input name="id" value={modal.id} readOnly hidden/>
<input name="loc_text" value={locationObj.text} readOnly hidden/>
<input name="loc_lat" value={locationObj.lat ? locationObj.lat : -142} readOnly hidden/>
<input name="loc_lng" value={locationObj.lng ? locationObj.lng : -142} readOnly hidden/>
<div className="row">
<div className="input-group mb-3">
<label className="input-group-text">Adresse</label>
<input className="form-control" aria-autocomplete="list" aria-expanded="true" autoComplete="true"
placeholder="Chercher une adresse..." aria-label="Recherche" list="addr" value={location}
onChange={e => setLocation(e.target.value)}/>
<datalist id="addr">
{data?.features && data.features.map((d, index) => {
return <option key={index}>{d.properties.label}</option>
})}
</datalist>
</div>
<div className="row">
<MapContainer ref={map} center={mapPosition} zoom={13} scrollWheelZoom={true} style={{height: "30em"}}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{locationObj.lat !== undefined && <Marker position={mapPosition}/>}
</MapContainer>
</div>
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" disabled={locationObj.lng === undefined}>Enregistrer</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
</div>
</div>
</form>
</div>
<div className="row">
<MapContainer ref={map} center={mapPosition} zoom={13} scrollWheelZoom={true} style={{height: "30em"}}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{locationObj.lat !== undefined && <Marker position={mapPosition}/>}
</MapContainer>
</div>
</>
</div>
}
proj4.defs("EPSG:9794", "+proj=lcc +lat_1=44 +lat_2=49 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");
function convertLambert93ToLatLng(x, y) {
const lambertPoint = proj4.toPoint([x, y]);
const wgs84Point = proj4("EPSG:9794", "EPSG:4326", lambertPoint);
return {lat: wgs84Point.y, lng: wgs84Point.x};
}

View File

@ -6,13 +6,12 @@ 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 {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {useEffect, useReducer, useRef, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
import {LocationEditor, LocationEditorModal} from "./LocationEditor.jsx";
import {useRef, useState} from "react";
import {LocationEditor, LocationEditorModal} from "../../../components/Club/LocationEditor.jsx";
import {ContactEditor} from "../../../components/Club/ContactEditor.jsx";
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
const vite_url = import.meta.env.VITE_URL;
@ -78,7 +77,7 @@ function InformationForm({data}) {
const formData = new FormData(event.target);
toast.promise(
apiAxios.post(`/club/${data.id}`, formData),
apiAxios.put(`/club/${data.id}`, formData),
{
pending: "Enregistrement du club en cours",
success: "Club enregistrée avec succès 🎉",
@ -90,46 +89,55 @@ function InformationForm({data}) {
return <>
<form onSubmit={handleSubmit}>
<div className="card mb-4">
<input name="id" value={data.id} readOnly hidden/>
<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}/>
<CountryList name="country" text="Pays" value={data.country}/>
<img
src={`${vite_url}/api/club/${data.id}/logo`}
src={`${vite_url}/api/club/${data.clubId}/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"
<label className="input-group-text" htmlFor="logo">Blason</label>
<input type="file" className="form-control" id="logo" name="logo"
accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div>
<div className="form-text" id="url_photo">Laissez vide pour ne rien changer.</div>
<div className="form-text" id="logo">Laissez vide pour ne rien changer.</div>
</div>
<div className="input-group mb-3">
<div className="input-group-text">
<input type="checkbox" id="inputGroupSelect01" className="form-check-input mt-0" name="international"
<input type="checkbox" className="form-check-input mt-0" name="international" id="international"
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
</div>
<label className="input-group-text" htmlFor="inputGroupSelect01">Club externe</label>
<label className="input-group-text" htmlFor="international">Club externe</label>
</div>
{!switchOn && <>
<TextField name="siret" text="SIRET" value={data.siret} type="number"/>
<TextField name="rna" text="RNA" value={data.rna} required={false}/>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}/>
<div className="mb-3">
<div className="input-group">
<label className="input-group-text" htmlFor="status">Status</label>
<input type="file" className="form-control" id="status" name="status"
accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div>
<div className="form-text" id="status">Laissez vide pour ne rien changer.</div>
</div>
<ContactEditor data={data}/>
<LocationEditor data={data} setModal={setModal} sendData={locationModalCallback}/>
<HoraireEditor data={data}/>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false}/>
</>
}
</div>
<div className="row">
<div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button>
</div>
@ -140,134 +148,3 @@ function InformationForm({data}) {
<LocationEditorModal modal={modal} sendData={locationModalCallback}/>
</>
}
export function ContactEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
useEffect(() => {
for (const key in data.contact) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: key, data: data.contact[key]}})
}
}, [data.contact]);
useEffect(() => {
let out_data2 = {}
state.forEach(d => {
if (d.data !== undefined)
out_data2[d.id] = d.data
})
setOutData(out_data2)
}, [state]);
return <div className="row">
<input name="contact" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Contacts</span>
<div className="input-group mb-3">
<ul className="list-group form-control">
{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>
</div>
</div>
}
function timeNumberToSting(nbMin) {
return String(Math.floor(nbMin / 60)).padStart(2, '0') + ":" + String(nbMin % 60).padStart(2, '0')
}
function timeStringToNumber(time) {
let times = time.split(':');
return parseInt(times[0]) * 60 + parseInt(times[1]);
}
export function HoraireEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
useEffect(() => {
if (data.training_day_time === null)
return
JSON.parse(data.training_day_time).forEach((d, index) => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: index, data: d}})
})
}, [data.training_day_time]);
useEffect(() => {
setOutData(state.map(d => {
return {day: d.data.day, time_start: d.data.time_start, time_end: d.data.time_end}
}))
}, [state]);
return <div className="row">
<input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Horaires d'entrainements</span>
<div className="input-group mb-3">
<ul className="list-group form-control">
{state.map((d, index) => {
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.data.day}
onChange={(e) => {
d.data.day = e.target.value
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
}}>
<option value="0">Lundi</option>
<option value="1">Mardi</option>
<option value="2">Mercredi</option>
<option value="3">Jeudi</option>
<option value="4">Vendredi</option>
<option value="5">Samedi</option>
<option value="6">Dimanche</option>
</select>
<span className="input-group-text">de</span>
<input type="time" className="form-control" defaultValue={timeNumberToSting(d.data.time_start)} required
onChange={(e) => {
d.data.time_start = timeStringToNumber(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
}}/>
<span className="input-group-text">à</span>
<input type="time" className="form-control" defaultValue={timeNumberToSting(d.data.time_end)} required
onChange={(e) => {
d.data.time_end = timeStringToNumber(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
}}/>
<button className="btn btn-danger" type="button"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}><FontAwesomeIcon
icon={faTrashCan}/>
</button>
</div>
</div>
})}
</ul>
</div>
</div>
}

View File

@ -18,6 +18,8 @@ export function SimpleReducer(datas, action) {
datas[index] = action.payload
return [...datas]
}
case 'SORT':
return datas.sort(action.payload)
default:
throw new Error()
}