feat: club end main
This commit is contained in:
parent
e1a8c90f3e
commit
6a21bd4735
@ -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;
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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));
|
||||
|
||||
79
src/main/webapp/src/components/Club/ContactEditor.jsx
Normal file
79
src/main/webapp/src/components/Club/ContactEditor.jsx
Normal 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>
|
||||
}
|
||||
95
src/main/webapp/src/components/Club/HoraireEditor.jsx
Normal file
95
src/main/webapp/src/components/Club/HoraireEditor.jsx
Normal 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>
|
||||
}
|
||||
@ -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='© <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='© <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};
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user