From 7f7f4b2031e0d4d44916de73ec7fa415880f48de Mon Sep 17 00:00:00 2001 From: pprevautel Date: Wed, 9 Oct 2024 12:30:40 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Ajout=20de=20la=20gestion=20des=20membr?= =?UTF-8?q?es=20pour=20un=20guichet=20(d=C3=A9but)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/@types/app_espaceco.ts | 18 ++ assets/espaceco/api/community.ts | 21 ++- .../pages/communities/Communities.tsx | 4 +- .../pages/communities/ManageCommunity.tsx | 6 +- .../pages/communities/ManageCommunityTr.tsx | 3 + .../communities/management/Description.tsx | 4 +- .../pages/communities/management/Grid.tsx | 21 ++- .../pages/communities/management/GridList.tsx | 15 +- .../pages/communities/management/Members.tsx | 159 ++++++++++++++++++ .../pages/communities/management/Reports.tsx | 2 +- .../management/ZoomAndCentering.tsx | 70 ++++---- .../ZoomAndCentering/ExtentDialog.tsx | 99 +++++------ .../management/ZoomAndCentering/RMap.tsx | 50 +++--- .../ZoomAndCentering/SearchGrids.tsx | 32 +++- .../communities/management/validationTr.tsx | 3 + assets/i18n/i18n.ts | 3 +- assets/i18n/languages/en.tsx | 2 + assets/i18n/languages/fr.tsx | 2 + assets/modules/espaceco/RQKeys.ts | 15 +- .../EspaceCo/CommunityController.php | 27 +++ .../EspaceCoApi/CommunityApiService.php | 49 ++++++ src/Services/EspaceCoApi/UserApiService.php | 5 + 22 files changed, 474 insertions(+), 136 deletions(-) create mode 100644 assets/espaceco/pages/communities/management/Members.tsx diff --git a/assets/@types/app_espaceco.ts b/assets/@types/app_espaceco.ts index 0844a267..1e564fb2 100644 --- a/assets/@types/app_espaceco.ts +++ b/assets/@types/app_espaceco.ts @@ -46,3 +46,21 @@ export type SearchGridFilters = { fields?: ("name" | "title" | "type" | "extent" | "deleted")[]; adm?: boolean; }; + +export type CommunityMember = { + user_id: number; + username: string; + firstname: string | null; + surname: string | null; + emprises: string[]; // TODO renommer en grids + role: "pending" | "member" | "admin"; + active: boolean; + date: string; +}; + +/* FORMULAIRES */ +export type DescriptionFormType = { + name: string; + description?: string; + keywords?: string[]; +}; diff --git a/assets/espaceco/api/community.ts b/assets/espaceco/api/community.ts index 724b56d2..02e0a638 100644 --- a/assets/espaceco/api/community.ts +++ b/assets/espaceco/api/community.ts @@ -1,6 +1,6 @@ import SymfonyRouting from "../../modules/Routing"; -import { CommunityListFilter, GetResponse } from "../../@types/app_espaceco"; +import { CommunityListFilter, CommunityMember, GetResponse } from "../../@types/app_espaceco"; import { type CommunityResponseDTO } from "../../@types/espaceco"; import { jsonFetch } from "../../modules/jsonFetch"; @@ -13,6 +13,11 @@ const get = (queryParams: { page: number; limit: number }, signal: AbortSignal) }); }; +const getCommunitiesName = () => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_names"); + return jsonFetch(url); +}; + const searchByName = (name: string, filter: CommunityListFilter, signal: AbortSignal) => { const queryParams = { name: `%${name}%`, filter: filter, sort: "name:ASC" }; const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_search", queryParams); @@ -34,6 +39,18 @@ const getCommunity = (communityId: number) => { return jsonFetch(url); }; +const getCommunityMembers = (communityId: number, page: number, limit: number = 10, signal: AbortSignal) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_members", { communityId, page: page, limit: limit }); + return jsonFetch>(url, { + signal: signal, + }); +}; + +const getCommunityMembershipRequests = (communityId: number) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_members", { communityId, page: 1, limit: 50, roles: ["pending"] }); + return jsonFetch(url); +}; + const updateLogo = (communityId: number, formData: FormData) => { const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_update_logo", { communityId }); return jsonFetch( @@ -49,6 +66,6 @@ const updateLogo = (communityId: number, formData: FormData) => { ); }; -const community = { get, getCommunity, searchByName, getAsMember, updateLogo }; +const community = { get, getCommunitiesName, getCommunity, getCommunityMembers, getCommunityMembershipRequests, searchByName, getAsMember, updateLogo }; export default community; diff --git a/assets/espaceco/pages/communities/Communities.tsx b/assets/espaceco/pages/communities/Communities.tsx index ed83d0a3..5eaf6d59 100644 --- a/assets/espaceco/pages/communities/Communities.tsx +++ b/assets/espaceco/pages/communities/Communities.tsx @@ -51,7 +51,7 @@ const Communities: FC = () => { const [community, setCommunity] = useState(null); const communityQuery = useQuery, CartesApiException>({ - queryKey: RQKeys.community_list(queryParams.page, queryParams.limit), + queryKey: RQKeys.communityList(queryParams.page, queryParams.limit), queryFn: ({ signal }) => api.community.get(queryParams, signal), staleTime: 3600000, //retry: false, @@ -59,7 +59,7 @@ const Communities: FC = () => { }); const communitiesAsMember = useQuery, CartesApiException>({ - queryKey: RQKeys.communities_as_member(queryParams.pending ?? false, queryParams.page, queryParams.limit), + queryKey: RQKeys.communitiesAsMember(queryParams.pending ?? false, queryParams.page, queryParams.limit), queryFn: ({ signal }) => api.community.getAsMember(queryParams, signal), staleTime: 3600000, //retry: false, diff --git a/assets/espaceco/pages/communities/ManageCommunity.tsx b/assets/espaceco/pages/communities/ManageCommunity.tsx index b31a3685..c2f3e7a9 100644 --- a/assets/espaceco/pages/communities/ManageCommunity.tsx +++ b/assets/espaceco/pages/communities/ManageCommunity.tsx @@ -18,6 +18,7 @@ import Grid from "./management/Grid"; import Layer from "./management/Layer"; import Reports from "./management/Reports"; import ZoomAndCentering from "./management/ZoomAndCentering"; +import Members from "./management/Members"; type ManageCommunityProps = { communityId: number; @@ -78,6 +79,7 @@ const ManageCommunity: FC = ({ communityId }) => { { tabId: "tab5", label: t("tab5") }, // Outils { tabId: "tab6", label: t("tab6") }, // Signalements { tabId: "tab7", label: t("tab7") }, // Emprises + { tabId: "tab8", label: t("tab8") }, // Membres ]} onTabChange={setSelectedTabId} > @@ -93,7 +95,9 @@ const ManageCommunity: FC = ({ communityId }) => { case "tab6": return ; case "tab7": - return ; // TODO + return ; // TODO + case "tab8": + return ; default: return

`Content of ${selectedTabId}`

; } diff --git a/assets/espaceco/pages/communities/ManageCommunityTr.tsx b/assets/espaceco/pages/communities/ManageCommunityTr.tsx index b9928b56..fbb713ed 100644 --- a/assets/espaceco/pages/communities/ManageCommunityTr.tsx +++ b/assets/espaceco/pages/communities/ManageCommunityTr.tsx @@ -17,6 +17,7 @@ export const { i18n } = declareComponentKeys< | "tab5" | "tab6" | "tab7" + | "tab8" | "desc.tab.title" | "desc.name" | "desc.hint_name" @@ -86,6 +87,7 @@ export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity" tab5: "Outils", tab6: "Signalements", tab7: "Emprises", + tab8: "Membres", "desc.tab.title": "Décrire le guichet", "desc.name": "Nom du guichet", "desc.hint_name": "Donnez un nom clair et compréhensible", @@ -188,6 +190,7 @@ export const ManageCommunityEnTranslations: Translations<"en">["ManageCommunity" tab5: undefined, tab6: undefined, tab7: undefined, + tab8: undefined, "desc.tab.title": undefined, "desc.name": undefined, "desc.hint_name": undefined, diff --git a/assets/espaceco/pages/communities/management/Description.tsx b/assets/espaceco/pages/communities/management/Description.tsx index 3c58fdbe..c0d23107 100644 --- a/assets/espaceco/pages/communities/management/Description.tsx +++ b/assets/espaceco/pages/communities/management/Description.tsx @@ -13,10 +13,10 @@ import MarkdownEditor from "../../../../components/Input/MarkdownEditor"; import thumbnails from "../../../../data/doc_thumbnail.json"; import categories from "../../../../data/topic_categories.json"; import { ComponentKey, useTranslation } from "../../../../i18n/i18n"; +import { appRoot } from "../../../../router/router"; import { getFileExtension } from "../../../../utils"; import { AddDocumentDialog, AddDocumentDialogModal } from "./AddDocumentDialog"; import CommunityLogo from "./CommunityLogo"; -import { appRoot } from "../../../../router/router"; import "../../../../sass/pages/espaceco/community.scss"; @@ -41,6 +41,8 @@ const readFileAsDataURL = async (file: File) => { }; */ const Description: FC = ({ community }) => { + // const { tab1 } = useCommunityFormStore(community)(); + const { t: tCommon } = useTranslation("Common"); const { t: tValid } = useTranslation("ManageCommunityValidations"); const { t } = useTranslation("ManageCommunity"); diff --git a/assets/espaceco/pages/communities/management/Grid.tsx b/assets/espaceco/pages/communities/management/Grid.tsx index 65c56269..afefb759 100644 --- a/assets/espaceco/pages/communities/management/Grid.tsx +++ b/assets/espaceco/pages/communities/management/Grid.tsx @@ -1,14 +1,28 @@ import { FC } from "react"; -import { useTranslation } from "../../../../i18n/i18n"; +import { useForm } from "react-hook-form"; import { Grid } from "../../../../@types/espaceco"; +import { useTranslation } from "../../../../i18n/i18n"; import GridList from "./GridList"; type GridProps = { grids: Grid[]; }; +type GridForm = { + grids: string[]; +}; + const Grid: FC = ({ grids }) => { const { t } = useTranslation("ManageCommunity"); + + const form = useForm({ + mode: "onSubmit", + values: { + grids: Array.from(grids, (g) => g.name), + }, + }); + const { setValue: setFormValue } = form; + return ( <>

{t("grid.grids")}

@@ -16,7 +30,10 @@ const Grid: FC = ({ grids }) => { { - console.log(grids); // TODO + setFormValue( + "grids", + Array.from(grids, (g) => g.name) + ); }} /> diff --git a/assets/espaceco/pages/communities/management/GridList.tsx b/assets/espaceco/pages/communities/management/GridList.tsx index 09a82504..1ca37971 100644 --- a/assets/espaceco/pages/communities/management/GridList.tsx +++ b/assets/espaceco/pages/communities/management/GridList.tsx @@ -12,8 +12,6 @@ type GridListProps = { const GridList: FC = ({ grids = [], onChange }) => { const [grid, setGrid] = useState(null); - console.log(grid); - const [internal, setInternal] = useState([...grids]); const handleRemove = useCallback( @@ -27,8 +25,9 @@ const GridList: FC = ({ grids = [], onChange }) => { const handleAdd = () => { if (grid) { - const grids = [...internal, grid]; + const grids = Array.from(new Set([...internal, grid])); setInternal(grids); + onChange(grids); } }; const data = useMemo(() => { @@ -42,18 +41,22 @@ const GridList: FC = ({ grids = [], onChange }) => { return (
-
+
setGrid(grid)} + onChange={(grid) => { + if (grid) { + setGrid(grid); + } + }} />
-
+
diff --git a/assets/espaceco/pages/communities/management/Members.tsx b/assets/espaceco/pages/communities/management/Members.tsx new file mode 100644 index 00000000..4e4cb0f6 --- /dev/null +++ b/assets/espaceco/pages/communities/management/Members.tsx @@ -0,0 +1,159 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Table from "@codegouvfr/react-dsfr/Table"; +import Pagination from "@mui/material/Pagination"; +import { useQuery } from "@tanstack/react-query"; +import { FC, ReactNode, useMemo, useState } from "react"; +import { CommunityMember, GetResponse } from "../../../../@types/app_espaceco"; +import LoadingText from "../../../../components/Utils/LoadingText"; +import { declareComponentKeys, Translations, useTranslation } from "../../../../i18n/i18n"; +import RQKeys from "../../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../../modules/jsonFetch"; +import { routes } from "../../../../router/router"; +import api from "../../../api"; +import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; + +export type membersQueryParams = { + page: number; + limit: number; +}; + +type MembersProps = { + communityId: number; +}; + +const maxFetchedMembers = 10; +const getName = (firstname: string | null, surname: string | null) => `${firstname ? firstname : ""} ${surname ? surname : ""}`; + +const Members: FC = ({ communityId }) => { + const { t } = useTranslation("EscoCommunityMembers"); + + const [queryParams, setQueryParams] = useState({ page: 1, limit: maxFetchedMembers }); + + // Les demandes d'affiliation + const membershipRequestsQuery = useQuery({ + queryKey: RQKeys.communityMembershipRequests(communityId), + queryFn: () => api.community.getCommunityMembershipRequests(communityId), + staleTime: 60000, + }); + + // Les membres non en demande d'affiliation + const membersQuery = useQuery, CartesApiException>({ + queryKey: RQKeys.communityMembers(communityId, queryParams.page, queryParams.limit), + queryFn: ({ signal }) => api.community.getCommunityMembers(communityId, queryParams.page, queryParams.limit, signal), + staleTime: 60000, + }); + + const headers = useMemo(() => [t("username_header"), t("name_header"), t("status_header"), t("grids_header"), ""], [t]); + const data: ReactNode[][] = useMemo(() => { + return ( + membersQuery.data?.content.map((m) => [ + m.username, + getName(m.firstname, m.surname), + , + "titi", +
+
, + ]) ?? [] + ); + }, [membersQuery.data]); + + return ( +
+ {membershipRequestsQuery.isError && ( + +

{membershipRequestsQuery.error?.message}

+ + + } + /> + )} + {membersQuery.isError && ( + +

{membersQuery.error?.message}

+ + + } + /> + )} + {membershipRequestsQuery.isLoading || membersQuery.isLoading ? ( + + ) : membershipRequestsQuery.data && membershipRequestsQuery.data.length > 0 ? ( + + contenu + + ) : ( + membersQuery.data?.content && + membersQuery.data.content.length > 0 && ( +
+ +
+ + setQueryParams({ ...queryParams, page: v })} + /> + +
+ + ) + )} + + ); +}; + +export default Members; + +// traductions +export const { i18n } = declareComponentKeys< + | "fetch_failed" + | "back_to_list" + | "loading_members" + | "loading_membership_requests" + | { K: "membership_requests"; P: { count: number }; R: string } + | "username_header" + | "name_header" + | "status_header" + | "grids_header" +>()("EscoCommunityMembers"); + +export const EscoCommunityMembersFrTranslations: Translations<"fr">["EscoCommunityMembers"] = { + fetch_failed: "La récupération des membres du guichet a échoué", + back_to_list: "Retour à la liste des guichets", + loading_members: "Chargement des membres du guichet", + loading_membership_requests: "Chargement des demandes d’affiliation", + membership_requests: ({ count }) => `Demandes d’affiliation (${count})`, + username_header: "Nom de l'utilisateur", + name_header: "Nom, prénom", + status_header: "Statut", + grids_header: "Emprises individuelles", +}; + +export const EscoCommunityMembersEnTranslations: Translations<"en">["EscoCommunityMembers"] = { + fetch_failed: undefined, + back_to_list: undefined, + loading_members: undefined, + loading_membership_requests: undefined, + membership_requests: ({ count }) => `Membership requests (${count})`, + username_header: "username", + name_header: undefined, + status_header: "Status", + grids_header: undefined, +}; diff --git a/assets/espaceco/pages/communities/management/Reports.tsx b/assets/espaceco/pages/communities/management/Reports.tsx index 28e62f9b..16f57590 100644 --- a/assets/espaceco/pages/communities/management/Reports.tsx +++ b/assets/espaceco/pages/communities/management/Reports.tsx @@ -127,7 +127,7 @@ const Reports: FC = ({ community }) => { }); const sharedThemesQuery = useQuery({ - queryKey: RQKeys.user_shared_themes(), + queryKey: RQKeys.userSharedThemes(), queryFn: () => api.user.getSharedThemes(), staleTime: 3600000, }); diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx index ef1e0167..39fae25a 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx @@ -1,11 +1,10 @@ import { fr } from "@codegouvfr/react-dsfr"; import Alert from "@codegouvfr/react-dsfr/Alert"; import Button from "@codegouvfr/react-dsfr/Button"; -import { Coordinate } from "ol/coordinate"; import { containsCoordinate, Extent } from "ol/extent"; import WKT from "ol/format/WKT"; -import { toLonLat } from "ol/proj"; -import { FC, useEffect, useState } from "react"; +import { FC, useCallback, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; import { CommunityResponseDTO } from "../../../../@types/espaceco"; import ZoomRange from "../../../../components/Utils/ZoomRange"; import olDefaults from "../../../../data/ol-defaults.json"; @@ -18,12 +17,12 @@ type ZoomAndCenteringProps = { community: CommunityResponseDTO; }; -type formType = { - position: Coordinate | null; +export type ZoomAndCenteringFormType = { + position: number[]; zoom: number; zoomMin: number; zoomMax: number; - extent: Extent | null; + extent?: Extent | null; }; const ZoomAndCentering: FC = ({ community }) => { @@ -33,33 +32,40 @@ const ZoomAndCentering: FC = ({ community }) => { // Cohérence entre l'extent et la position const [consistent, setConsistent] = useState(true); - const [values, setValues] = useState(() => { - let p = null; + const getValues = useCallback(() => { + let p; if (community.position) { const feature = new WKT().readFeature(community.position, { dataProjection: "EPSG:4326", }); p = feature.getGeometry().getCoordinates(); - } - - const zoomMin = community.zoom_min ?? olDefaults.zoom_levels.TOP; - const zoomMax = community.zoom_max ?? olDefaults.zoom_levels.BOTTOM; + } else p = olDefaults.center; return { position: p, + zoom: community.zoom ?? olDefaults.zoom, + zoomMin: community.zoom_min ?? olDefaults.zoom_levels.TOP, + zoomMax: community.zoom_max ?? olDefaults.zoom_levels.BOTTOM, extent: community.extent, - zoomMin: zoomMin, - zoomMax: zoomMax, - zoom: zoomMax, }; + }, [community]); + + const form = useForm({ + mode: "onSubmit", + values: getValues(), }); + const { watch, getValues: getFormValues, setValue: setFormValue } = form; + console.log(watch()); + + const position = watch("position"); + const extent = watch("extent"); useEffect(() => { - if (values.extent && values.position) { - setConsistent(containsCoordinate(values.extent, values.position)); + if (position && extent) { + setConsistent(containsCoordinate(extent, position)); } return; - }, [values]); + }, [position, extent]); return (
@@ -78,7 +84,7 @@ const ZoomAndCentering: FC = ({ community }) => { }} onChange={(newPosition) => { if (newPosition) { - setValues({ ...values, position: newPosition }); + setFormValue("position", newPosition); } }} /> @@ -88,10 +94,12 @@ const ZoomAndCentering: FC = ({ community }) => { small={true} min={olDefaults.zoom_levels.TOP} max={olDefaults.zoom_levels.BOTTOM} - values={[values.zoomMin, values.zoomMax]} + values={[getFormValues("zoomMin"), getFormValues("zoomMax")]} onChange={(v) => { - const { zoom } = values; - setValues({ ...values, zoomMin: v[0], zoomMax: v[1], zoom: zoom < v[1] ? zoom : v[1] }); + const oldZoom = getFormValues("zoom"); + setFormValue("zoomMin", v[0]); + setFormValue("zoomMax", v[1]); + setFormValue("zoom", oldZoom < v[1] ? oldZoom : v[1]); }} />
@@ -108,25 +116,15 @@ const ZoomAndCentering: FC = ({ community }) => {
{ - const v = { - position: toLonLat(center), - }; - if (zoom) { - v["zoom"] = zoom; - } - setValues({ ...values, ...v }); - }} + form={form} + onPositionChanged={(position) => setFormValue("position", position)} + onZoomChanged={(zoom) => setFormValue("zoom", zoom)} />
ExtentDialogModal.close()} onApply={(e) => { - setValues({ ...values, extent: e }); + setFormValue("extent", e); ExtentDialogModal.close(); }} /> diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx index 811eb68b..c5ae68c2 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx @@ -39,7 +39,8 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { const [choice, setChoice] = useState("manual"); - const schema = yup.object({ + const schema = {}; + schema["manual"] = yup.object({ xmin: yup .number() .typeError(tValid("zoom.extent.nan", { field: "${path}" })) @@ -104,10 +105,13 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { }, }), }); + schema["autocomplete"] = yup.object({ + extent: yup.array().of(yup.number()).required(tValid("zoom.extent.required")), + }); const form = useForm({ mode: "onChange", - resolver: yupResolver(schema), + resolver: yupResolver(schema[choice]), }); const { register, @@ -181,56 +185,53 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { ]} /> {choice === "autocomplete" ? ( - { - if (grid) { - setFormValue("xmin", grid.extent?.[0]); - setFormValue("ymin", grid.extent?.[1]); - setFormValue("xmax", grid.extent?.[2]); - setFormValue("ymax", grid.extent?.[3]); - } else clear(); - clearErrors(); - }} - /> - ) : ( - - )} -
-
- - + { + setFormValue("extent", grid ? grid : undefined); + clearErrors(); + }} />
-
- - + ) : ( +
+ +
+
+ + +
+
+ + +
+
-
+ )} , document.body diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx index 050850b9..22f44841 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx @@ -3,52 +3,50 @@ import { defaults as defaultControls, ScaleLine } from "ol/control"; import { Coordinate } from "ol/coordinate"; import Point from "ol/geom/Point"; import { DragPan, MouseWheelZoom } from "ol/interaction"; +import BaseLayer from "ol/layer/Base"; import TileLayer from "ol/layer/Tile"; import VectorLayer from "ol/layer/Vector"; import Map from "ol/Map"; -import { fromLonLat } from "ol/proj"; +import { fromLonLat, toLonLat } from "ol/proj"; import VectorSource from "ol/source/Vector"; import WMTS, { optionsFromCapabilities } from "ol/source/WMTS"; import Icon from "ol/style/Icon"; import Style from "ol/style/Style"; import View from "ol/View"; import { CSSProperties, FC, useEffect, useMemo, useRef } from "react"; +import { UseFormReturn } from "react-hook-form"; import olDefaults from "../../../../../data/ol-defaults.json"; import useCapabilities from "../../../../../hooks/useCapabilities"; import punaise from "../../../../../img/punaise.png"; -import DisplayCenterControl from "../../../../../ol/controls/DisplayCenterControl"; -import BaseLayer from "ol/layer/Base"; +import { ZoomAndCenteringFormType } from "../ZoomAndCentering"; const mapStyle: CSSProperties = { height: "400px", }; type RMapProps = { - position: Coordinate | null; - // NOTE Supprimé car si la position n'est pas dans l'extent, le centre de la carte (position) est déplacé - // extent?: Extent; - zoom: number; - zoomMin: number; - zoomMax: number; - onMove: (center: Coordinate, zoom?: number) => void; + form: UseFormReturn; + onPositionChanged: (position: Coordinate) => void; + onZoomChanged: (zoom: number) => void; }; -const RMap: FC = ({ position, zoom, zoomMin, zoomMax, onMove }) => { +const RMap: FC = ({ form, onPositionChanged, onZoomChanged }) => { const mapTargetRef = useRef(null); const mapRef = useRef(); // Création de la couche openlayers de fond (bg layer) const { data: capabilities } = useCapabilities(); - const center = useMemo(() => { - return position ? fromLonLat(position) : fromLonLat(olDefaults.center); - }, [position]); + const { watch, getValues: getFormValues } = form; + const position = watch("position"); + + const position3857 = useMemo(() => fromLonLat(position), [position]); // Création de la carte une fois bg layer créée useEffect(() => { if (!capabilities) return; - const feature = new Feature(new Point(center)); + const feature = new Feature(new Point(position3857)); // layer punaise const source = new VectorSource(); @@ -81,7 +79,7 @@ const RMap: FC = ({ position, zoom, zoomMin, zoomMax, onMove }) => { mapRef.current = new Map({ target: mapTargetRef.current as HTMLElement, layers: layers, - controls: defaultControls().extend([new ScaleLine(), new DisplayCenterControl({})]), + controls: defaultControls().extend([new ScaleLine()]), interactions: [ new DragPan(), new MouseWheelZoom({ @@ -89,10 +87,10 @@ const RMap: FC = ({ position, zoom, zoomMin, zoomMax, onMove }) => { }), ], view: new View({ - center: center, - zoom: zoom, - minZoom: zoomMin, - maxZoom: zoomMax, + center: position3857, + zoom: getFormValues("zoom"), + minZoom: getFormValues("zoomMin"), + maxZoom: getFormValues("zoomMax"), }), }); @@ -101,15 +99,17 @@ const RMap: FC = ({ position, zoom, zoomMin, zoomMax, onMove }) => { const centerView = map.getView().getCenter() as Coordinate; const z = map.getView().getZoom() as number; - // Rien n'a bougé - if (Math.abs(centerView[0] - center[0]) < 1 && Math.abs(centerView[1] - center[1]) < 1) { - return; + if (z !== getFormValues("zoom")) { + onZoomChanged(Math.round(z)); + } + + if (Math.abs(centerView[0] - position3857[0]) > 1 && Math.abs(centerView[1] - position3857[1]) > 1) { + onPositionChanged(toLonLat(centerView)); } - onMove(centerView, Math.round(z) !== zoom ? z : undefined); }); return () => mapRef.current?.setTarget(undefined); - }, [capabilities, center, zoom, zoomMin, zoomMax, onMove]); + }, [capabilities, position3857, getFormValues, onPositionChanged, onZoomChanged]); return
; }; diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx index 035c820d..4c088c2f 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx @@ -1,6 +1,8 @@ import { fr } from "@codegouvfr/react-dsfr"; import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; import Autocomplete, { autocompleteClasses } from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import TextField from "@mui/material/TextField"; import { useQuery } from "@tanstack/react-query"; import { FC, ReactNode } from "react"; import { useDebounceValue } from "usehooks-ts"; @@ -9,17 +11,17 @@ import { Grid } from "../../../../../@types/espaceco"; import { useTranslation } from "../../../../../i18n/i18n"; import RQKeys from "../../../../../modules/espaceco/RQKeys"; import api from "../../../../api"; -import TextField from "@mui/material/TextField"; -import Box from "@mui/material/Box"; export type SearchGridsProps = { label: ReactNode; hintText?: ReactNode; filters: SearchGridFilters; + state?: "default" | "error" | "success"; + stateRelatedMessage?: string; onChange: (grid: Grid | null) => void; }; -const SearchGrids: FC = ({ label, hintText, filters, onChange }) => { +const SearchGrids: FC = ({ label, hintText, filters, state, stateRelatedMessage, onChange }) => { const { t } = useTranslation("Search"); const [text, setText] = useDebounceValue("", 500); @@ -34,7 +36,7 @@ const SearchGrids: FC = ({ label, hintText, filters, onChange }); return ( -
+
); }; diff --git a/assets/espaceco/pages/communities/management/validationTr.tsx b/assets/espaceco/pages/communities/management/validationTr.tsx index cb46fd0e..52d47035 100644 --- a/assets/espaceco/pages/communities/management/validationTr.tsx +++ b/assets/espaceco/pages/communities/management/validationTr.tsx @@ -17,6 +17,7 @@ export const { i18n } = declareComponentKeys< | { K: "zoom.f1_less_than_f2"; P: { field1: string; field2: string }; R: string } | { K: "zoom.less_than"; P: { field: string; v: number }; R: string } | { K: "zoom.greater_than"; P: { field: string; v: number }; R: string } + | "zoom.extent.required" | "description.modal.document.name.mandatory" | "description.modal.document.name.minlength" | "description.modal.document.file.mandatory" @@ -38,6 +39,7 @@ export const ManageCommunityValidationsFrTranslations: Translations<"fr">["Manag "zoom.f1_less_than_f2": ({ field1, field2 }) => `La valeur de ${field1} doit être inférieure à la valeur de ${field2}`, "zoom.less_than": ({ field, v }) => `La valeur de ${field} doit être inférieure ou égale à ${v}`, "zoom.greater_than": ({ field, v }) => `La valeur de ${field} doit être supérieure ou égale à ${v}`, + "zoom.extent.required": "La boîte englobante est obligatoire", "description.modal.document.name.mandatory": "Le nom est obligatoire", "description.modal.document.name.minlength": "Le nom doit faire au moins 7 caractères", "description.modal.document.file.mandatory": "Le fichier est obligatoire", @@ -59,6 +61,7 @@ export const ManageCommunityValidationsEnTranslations: Translations<"en">["Manag "zoom.f1_less_than_f2": ({ field1, field2 }) => `${field1} value must be less then ${field2} value`, "zoom.less_than": ({ field, v }) => `${field} value must be less or equal to ${v}`, "zoom.greater_than": ({ field, v }) => `${field} value must be greater or equal to ${v}`, + "zoom.extent.required": undefined, "description.modal.document.name.mandatory": undefined, "description.modal.document.name.minlength": undefined, "description.modal.document.file.mandatory": undefined, diff --git a/assets/i18n/i18n.ts b/assets/i18n/i18n.ts index 1aa7b02b..42e8a37a 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -56,7 +56,8 @@ export type ComponentKey = | typeof import("../espaceco/pages/communities/management/SearchTr").i18n | typeof import("../espaceco/pages/communities/management/reports/ThemeTr").i18n | typeof import("../espaceco/pages/communities/management/reports/ReportStatusesTr").i18n - | typeof import("../espaceco/pages/communities/management/reports/SharedThemes").i18n; + | typeof import("../espaceco/pages/communities/management/reports/SharedThemes").i18n + | typeof import("../espaceco/pages/communities/management/Members").i18n; export type Translations = GenericTranslations; export type LocalizedString = Parameters[0]; diff --git a/assets/i18n/languages/en.tsx b/assets/i18n/languages/en.tsx index 588fa096..2af4e670 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -39,6 +39,7 @@ import { StyleEnTranslations } from "../Style"; import { ThemeEnTranslations } from "../../espaceco/pages/communities/management/reports/ThemeTr"; import { ReportStatusesEnTranslations } from "../../espaceco/pages/communities/management/reports/ReportStatusesTr"; import { SharedThemesEnTranslations } from "../../espaceco/pages/communities/management/reports/SharedThemes"; +import { EscoCommunityMembersEnTranslations } from "../../espaceco/pages/communities/management/Members"; import type { Translations } from "../i18n"; @@ -84,4 +85,5 @@ export const translations: Translations<"en"> = { ReportStatuses: ReportStatusesEnTranslations, SharedThemes: SharedThemesEnTranslations, Search: SearchEnTranslations, + EscoCommunityMembers: EscoCommunityMembersEnTranslations, }; diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index ad4301a1..86b4af1d 100644 --- a/assets/i18n/languages/fr.tsx +++ b/assets/i18n/languages/fr.tsx @@ -39,6 +39,7 @@ import { StyleFrTranslations } from "../Style"; import { ThemeFrTranslations } from "../../espaceco/pages/communities/management/reports/ThemeTr"; import { ReportStatusesFrTranslations } from "../../espaceco/pages/communities/management/reports/ReportStatusesTr"; import { SharedThemesFrTranslations } from "../../espaceco/pages/communities/management/reports/SharedThemes"; +import { EscoCommunityMembersFrTranslations } from "../../espaceco/pages/communities/management/Members"; import type { Translations } from "../i18n"; @@ -84,4 +85,5 @@ export const translations: Translations<"fr"> = { ReportStatuses: ReportStatusesFrTranslations, SharedThemes: SharedThemesFrTranslations, Search: SearchFrTranslations, + EscoCommunityMembers: EscoCommunityMembersFrTranslations, }; diff --git a/assets/modules/espaceco/RQKeys.ts b/assets/modules/espaceco/RQKeys.ts index d722a8dc..5b1103fc 100644 --- a/assets/modules/espaceco/RQKeys.ts +++ b/assets/modules/espaceco/RQKeys.ts @@ -1,18 +1,27 @@ import { CommunityListFilter } from "../../@types/app_espaceco"; const RQKeys = { - user_shared_themes: (): string[] => ["user", "shared_themes"], + communityList: (page: number, limit: number): string[] => ["communities", page.toString(), limit.toString()], + communitiesName: (): string[] => ["communities_names"], community: (communityId: number): string[] => ["community", communityId.toString()], - community_list: (page: number, limit: number): string[] => ["communities", page.toString(), limit.toString()], + communityMembershipRequests: (communityId: number): string[] => ["community", "members", "pending", communityId.toString()], + communityMembers: (communityId: number, page: number, limit: number): string[] => [ + "community", + "members", + communityId.toString(), + page.toString(), + limit.toString(), + ], searchCommunities: (search: string, filter: CommunityListFilter): string[] => { return ["searchCommunities", filter, search]; }, - communities_as_member: (pending: boolean, page: number, limit: number): string[] => [ + communitiesAsMember: (pending: boolean, page: number, limit: number): string[] => [ "communities_as_member", new Boolean(pending).toString(), page.toString(), limit.toString(), ], + userSharedThemes: (): string[] => ["user", "shared_themes"], searchAddress: (search: string): string[] => ["searchAddress", search], searchGrids: (text: string): string[] => ["searchGrids", text], tables: (communityId: number): string[] => ["feature_types", communityId.toString()], diff --git a/src/Controller/EspaceCo/CommunityController.php b/src/Controller/EspaceCo/CommunityController.php index a37ab18c..8c1761f5 100644 --- a/src/Controller/EspaceCo/CommunityController.php +++ b/src/Controller/EspaceCo/CommunityController.php @@ -45,6 +45,14 @@ public function get( } } + #[Route('/get_names', name: 'get_names', methods: ['GET'])] + public function getCommunitiesName(): JsonResponse + { + $names = $this->communityApiService->getCommunitiesName(); + + return new JsonResponse($names); + } + #[Route('/get_as_member', name: 'get_as_member', methods: ['GET'])] public function getMeMember( #[MapQueryParameter] bool $pending, @@ -121,6 +129,25 @@ public function getCommunity(int $communityId): JsonResponse } } + /** + * @param array $roles + */ + #[Route('/{communityId}/members', name: 'get_members', methods: ['GET'])] + public function getMembers( + int $communityId, + #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^admin|member|pending$/'])] array $roles = [], + #[MapQueryParameter] ?int $page = 1, + #[MapQueryParameter(options: ['min_range' => 1, 'max_range' => 50])] ?int $limit = 10 + ): JsonResponse { + try { + $response = $this->communityApiService->getCommunityMembers($communityId, $roles, $page, $limit); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + #[Route('/{communityId}/update_logo', name: 'update_logo', methods: ['PATCH'])] public function updateLogo(int $communityId, Request $request): JsonResponse { diff --git a/src/Services/EspaceCoApi/CommunityApiService.php b/src/Services/EspaceCoApi/CommunityApiService.php index c9a0f04c..0e19b732 100644 --- a/src/Services/EspaceCoApi/CommunityApiService.php +++ b/src/Services/EspaceCoApi/CommunityApiService.php @@ -24,6 +24,13 @@ public function getCommunities(string $name, int $page, int $limit, string $sort ]; } + public function getCommunitiesName(): array + { + $communities = $this->requestAll('communities', ['fields' => 'name']); + + return array_map(fn ($community) => $community['name'], $communities); + } + /** * @return array */ @@ -32,6 +39,48 @@ public function getCommunity(int $communityId): array return $this->request('GET', "communities/$communityId"); } + /** + * @param array $roles + * + * @return array + */ + public function getCommunityMembers(int $communityId, array $roles, int $page, int $limit): array + { + $query = ['fields' => 'user_id,emprises,role,active,date', 'page' => $page, 'limit' => $limit]; + if (count($roles)) { + $query['roles'] = $roles; + } + + $response = $this->request('GET', "communities/$communityId/members", [], $query, [], false, true, true); + + $contentRange = $response['headers']['content-range'][0]; + $totalPages = $this->getResultsPageCount($contentRange, $limit); + $previousPage = 1 === $page ? null : $page - 1; + $nextPage = $page + 1 > $totalPages ? null : $page + 1; + + $members = $response['content']; + foreach ($members as &$member) { + $userId = $member['user_id']; + $user = $this->request('GET', "users/$userId"); + $member = array_merge($member, $user); + } + + usort($members, function ($mb1, $mb2) { + if ($mb1['username'] == $mb2['username']) { + return 0; + } + + return (mb_strtolower($mb1['username'], 'UTF-8') < mb_strtolower($mb2['username'], 'UTF-8')) ? -1 : 1; + }); + + return [ + 'content' => $members, + 'totalPages' => $totalPages, + 'previousPage' => $previousPage, + 'nextPage' => $nextPage, + ]; + } + public function updateLogo(int $communityId, UploadedFile $file): array { return $this->request('PATCH', "communities/$communityId", ['logo' => $file], [], [], true); diff --git a/src/Services/EspaceCoApi/UserApiService.php b/src/Services/EspaceCoApi/UserApiService.php index 5b985d20..5aa3c938 100644 --- a/src/Services/EspaceCoApi/UserApiService.php +++ b/src/Services/EspaceCoApi/UserApiService.php @@ -18,4 +18,9 @@ public function getSharedThemes(): array return []; } + + public function getUser(int $userId): array + { + return $this->request('GET', "users/$userId"); + } }