From c8bfebb3782c0f8b9b17ca78d6198044d90c49e2 Mon Sep 17 00:00:00 2001 From: pprevautel Date: Wed, 24 Apr 2024 17:10:26 +0200 Subject: [PATCH] feat: Parcourir la liste des guichets ref #313 avec autocomplete --- assets/@types/app_espaceco.ts | 2 + assets/@types/espaceco.ts | 1 + assets/components/Utils/Pagination.tsx | 13 +- .../entrepot/pages/dashboard/DashboardPro.tsx | 2 +- assets/espaceco/api/community.ts | 28 ++- .../pages/communities/Communities.tsx | 163 ++++++++++++++++++ .../pages/communities/CommunitiesSkeleton.tsx | 16 ++ .../pages/communities/CommunityList.tsx | 90 ++-------- .../pages/communities/CommunityListItem.tsx | 62 +++++++ .../communities/EspaceCoCommunitiesTr.ts | 50 ++++++ .../pages/communities/SearchCommunity.tsx | 55 ++++++ assets/i18n/i18n.ts | 3 +- assets/i18n/languages/en.tsx | 2 + assets/i18n/languages/fr.tsx | 2 + assets/modules/espaceco/RQKeys.ts | 13 +- assets/router/RouterRenderer.tsx | 2 +- assets/sass/pages/espaceco/community.scss | 3 + package.json | 3 +- .../EspaceCo/CommunityController.php | 120 ++++++++++++- src/Controller/EspaceCo/UserController.php | 30 ++++ .../EspaceCoApi/CommunityApiService.php | 12 +- src/Services/EspaceCoApi/UserApiService.php | 11 ++ yarn.lock | 7 + 23 files changed, 601 insertions(+), 89 deletions(-) create mode 100644 assets/espaceco/pages/communities/Communities.tsx create mode 100644 assets/espaceco/pages/communities/CommunitiesSkeleton.tsx create mode 100644 assets/espaceco/pages/communities/CommunityListItem.tsx create mode 100644 assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts create mode 100644 assets/espaceco/pages/communities/SearchCommunity.tsx create mode 100644 assets/sass/pages/espaceco/community.scss create mode 100644 src/Controller/EspaceCo/UserController.php create mode 100644 src/Services/EspaceCoApi/UserApiService.php diff --git a/assets/@types/app_espaceco.ts b/assets/@types/app_espaceco.ts index 0568921f..a9cd2c68 100644 --- a/assets/@types/app_espaceco.ts +++ b/assets/@types/app_espaceco.ts @@ -4,3 +4,5 @@ export type GetResponse = { previousPage: number; nextPage: number; }; + +export type CommunityListFilter = "public" | "iam_member" | "affiliation"; diff --git a/assets/@types/espaceco.ts b/assets/@types/espaceco.ts index f114a89f..019b579f 100644 --- a/assets/@types/espaceco.ts +++ b/assets/@types/espaceco.ts @@ -1,6 +1,7 @@ export interface CommunityResponseDTO { id: number; description: string | null; + detailed_description?: string | null; name: string; active: boolean; shared_georem: "all" | "restrained" | "personal"; diff --git a/assets/components/Utils/Pagination.tsx b/assets/components/Utils/Pagination.tsx index b6a5059c..979f4396 100644 --- a/assets/components/Utils/Pagination.tsx +++ b/assets/components/Utils/Pagination.tsx @@ -15,11 +15,20 @@ type PaginationProps = { const Pagination: FC = (props: PaginationProps) => { const { page, count, shape = "rounded", size = "small", onChange } = props; - console.log("count : ", count); return ( - + ); }; diff --git a/assets/entrepot/pages/dashboard/DashboardPro.tsx b/assets/entrepot/pages/dashboard/DashboardPro.tsx index 1d97672a..738cd5fa 100644 --- a/assets/entrepot/pages/dashboard/DashboardPro.tsx +++ b/assets/entrepot/pages/dashboard/DashboardPro.tsx @@ -61,7 +61,7 @@ const DashboardPro = () => { {isApiEspaceCoDefined() && (
- +
)} diff --git a/assets/espaceco/api/community.ts b/assets/espaceco/api/community.ts index 62207b15..dabd71da 100644 --- a/assets/espaceco/api/community.ts +++ b/assets/espaceco/api/community.ts @@ -1,21 +1,33 @@ import SymfonyRouting from "../../modules/Routing"; -import { jsonFetch } from "../../modules/jsonFetch"; +import { CommunityListFilter, GetResponse } from "../../@types/app_espaceco"; import { type CommunityResponseDTO } from "../../@types/espaceco"; -import { GetResponse } from "../../@types/app_espaceco"; +import { jsonFetch } from "../../modules/jsonFetch"; -const get = (name: string, page: number, limit: number, signal: AbortSignal) => { - const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get", { - name: name, - page: page, - limit: limit, +const get = (queryParams: { page: number; limit: number }, signal: AbortSignal) => { + const params = { ...queryParams, sort: "name:ASC" }; + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get", params); + return jsonFetch>(url, { + signal: signal, }); +}; + +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); + return jsonFetch(url, { + signal: signal, + }); +}; +const getAsMember = (queryParams: Record, signal: AbortSignal) => { + const params = { ...queryParams, sort: "name:ASC" }; + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_as_member", params); return jsonFetch>(url, { signal: signal, }); }; -const community = { get }; +const community = { get, searchByName, getAsMember }; export default community; diff --git a/assets/espaceco/pages/communities/Communities.tsx b/assets/espaceco/pages/communities/Communities.tsx new file mode 100644 index 00000000..996d566a --- /dev/null +++ b/assets/espaceco/pages/communities/Communities.tsx @@ -0,0 +1,163 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import { Skeleton } from "@mui/material"; +import { useQuery } from "@tanstack/react-query"; +import { FC, useState } from "react"; +import { CommunityListFilter, GetResponse } from "../../../@types/app_espaceco"; +import { CommunityResponseDTO } from "../../../@types/espaceco"; +import Pagination from "../../../components/Utils/Pagination"; +import RQKeys from "../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../modules/jsonFetch"; +import api from "../../api"; +import CommunityList from "./CommunityList"; +import { useTranslation } from "../../../i18n/i18n"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import SearchCommunity from "./SearchCommunity"; + +const defaultLimit = 10; + +type commonParams = { + page: number; + limit: number; +}; + +type Pending = commonParams & { pending: boolean }; + +const Communities: FC = () => { + const { t } = useTranslation("EspaceCoCommunities"); + + const [filter, setFilter] = useState("public"); + + const [params, setParams] = useState({ page: 1, limit: defaultLimit }); + const [communitiesAsMemberParams, setCommunitiesAsMemberParams] = useState({ pending: false, page: 1, limit: defaultLimit }); + + const [community, setCommunity] = useState(null); + + // const { data /*, isError, error*/ } = useInfiniteQuery< + // GetResponse, + // CartesApiException, + // InfiniteData, number>, + // string[], + // number + // >({ + // queryKey: RQKeys.community_list("", page, limit), + // queryFn: ({ pageParam, signal }) => api.community.get(pageParam, limit, signal), + // initialPageParam: 1, + // getNextPageParam: (lastPage /*, allPages, lastPageParam, allPageParams*/) => lastPage.nextPage, + // getPreviousPageParam: (firstPage /*, allPages, firstPageParam, allPageParams*/) => firstPage.previousPage, + // }); + + const communityQuery = useQuery, CartesApiException>({ + queryKey: RQKeys.community_list(params.page, params.limit), + queryFn: ({ signal }) => api.community.get(params, signal), + staleTime: 3600000, + retry: false, + enabled: filter === "public", + }); + + const communitiesAsMember = useQuery, CartesApiException>({ + queryKey: RQKeys.communities_as_member(communitiesAsMemberParams.pending, communitiesAsMemberParams.page, communitiesAsMemberParams.limit), + queryFn: ({ signal }) => api.community.getAsMember(communitiesAsMemberParams, signal), + staleTime: 3600000, + retry: false, + enabled: filter === "iam_member" || filter === "affiliation", + }); + + const handleFilterChange = (filter: CommunityListFilter) => { + setFilter(filter); + setCommunity(null); + if (filter === "iam_member" || filter === "affiliation") { + // TODO A VOIR SI 2 REQUETES DIFFERENTES + setCommunitiesAsMemberParams({ pending: filter === "affiliation", page: 1, limit: defaultLimit }); + } + }; + + return ( +
+

{t("title")}

+
+ handleFilterChange("public"), + }, + }, + { + label: t("communities_as_member"), + nativeInputProps: { + checked: filter === "iam_member", + onChange: () => handleFilterChange("iam_member"), + }, + }, + { + label: t("pending_membership"), + nativeInputProps: { + checked: filter === "affiliation", + onChange: () => handleFilterChange("affiliation"), + }, + }, + ]} + orientation="horizontal" + /> +
+ {communityQuery.isLoading || communitiesAsMember.isLoading ? ( + + {[...Array(10).keys()].map((n) => ( + + ))} + + ) : communityQuery.isError ? ( + + ) : communitiesAsMember.isError ? ( + + ) : ( +
+ { + setCommunity(community); + }} + /> + {community ? ( + + ) : filter === "public" ? ( + communityQuery.data && ( +
+ +
+ setParams({ ...params, page: page, limit: defaultLimit })} + /> +
+
+ ) + ) : ( + communitiesAsMember.data && ( +
+ +
+ setCommunitiesAsMemberParams({ ...communitiesAsMemberParams, page: page, limit: defaultLimit })} + /> +
+
+ ) + )} +
+ )} +
+ ); +}; + +export default Communities; diff --git a/assets/espaceco/pages/communities/CommunitiesSkeleton.tsx b/assets/espaceco/pages/communities/CommunitiesSkeleton.tsx new file mode 100644 index 00000000..5d71efaa --- /dev/null +++ b/assets/espaceco/pages/communities/CommunitiesSkeleton.tsx @@ -0,0 +1,16 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Skeleton from "@mui/material/Skeleton"; +import { FC } from "react"; + +const CommunitiesSkeleton: FC = () => { + return ( + + {[...Array(10).keys()].map((n) => ( + + ))} + + ); +}; + +export default CommunitiesSkeleton; diff --git a/assets/espaceco/pages/communities/CommunityList.tsx b/assets/espaceco/pages/communities/CommunityList.tsx index 305e6ada..3e01f7f1 100644 --- a/assets/espaceco/pages/communities/CommunityList.tsx +++ b/assets/espaceco/pages/communities/CommunityList.tsx @@ -1,82 +1,30 @@ -import { useQuery } from "@tanstack/react-query"; -import { FC, useState } from "react"; -import { GetResponse } from "../../../@types/app_espaceco"; -import { CommunityResponseDTO } from "../../../@types/espaceco"; -import api from "../../../espaceco/api"; -import RQKeys from "../../../modules/espaceco/RQKeys"; -import { CartesApiException } from "../../../modules/jsonFetch"; -import Pagination from "../../../components/Utils/Pagination"; import { fr } from "@codegouvfr/react-dsfr"; -import LoadingText from "../../../components/Utils/LoadingText"; -import LoadingIcon from "../../../components/Utils/LoadingIcon"; -import Wait from "../../../components/Utils/Wait"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import { FC } from "react"; +import { CommunityResponseDTO } from "../../../@types/espaceco"; +import CommunityListItem from "./CommunityListItem"; +import { CommunityListFilter } from "../../../@types/app_espaceco"; import { useTranslation } from "../../../i18n/i18n"; -const limit = 10; - -type QueryParams = { - name: string; - currentPage: number; +type CommunityListProps = { + communities: CommunityResponseDTO[]; + filter: CommunityListFilter; }; -const CommunityList: FC = () => { - const [params, setParams] = useState({ name: "", currentPage: 1 }); - - const { t: tCommon } = useTranslation("Common"); - - // const { data /*, isError, error*/ } = useInfiniteQuery< - // GetResponse, - // CartesApiException, - // InfiniteData, number>, - // string[], - // number - // >({ - // queryKey: RQKeys.community_list("", currentPage, limit), - // queryFn: ({ pageParam, signal }) => api.community.get(pageParam, limit, signal), - // initialPageParam: 1, - // getNextPageParam: (lastPage /*, allPages, lastPageParam, allPageParams*/) => lastPage.nextPage, - // getPreviousPageParam: (firstPage /*, allPages, firstPageParam, allPageParams*/) => firstPage.previousPage, - // }); - - const communityQuery = useQuery, CartesApiException>({ - queryKey: RQKeys.community_list(params.name, params.currentPage, limit), - queryFn: ({ signal }) => api.community.get(params.name, params.currentPage, limit, signal), - staleTime: 20000, - retry: false, - enabled: params.name.length === 0 || params.name.length > 3, - }); - - console.log(communityQuery.data); +const CommunityList: FC = ({ communities, filter }) => { + const { t } = useTranslation("EspaceCoCommunities"); return ( -
- {communityQuery.isLoading ? ( - -
- - {tCommon("loading")} -
-
+
+ {communities.length === 0 ? ( +
+ +
) : ( - // - communityQuery.data?.content.length && ( - <> -
    {communityQuery.data?.content.map((community) =>
  • {community.name}
  • )}
-
- setParams({ ...params, currentPage: page })} - /*getPageLinkProps={(pageNumber) => { - return { - onClick: () => setParams({ ...params, currentPage: pageNumber }), - }; - }}*/ - /> -
- - ) + communities.map((community, index) => { + const className = index % 2 === 0 ? "frx-community-even" : ""; + return ; + }) )}
); diff --git a/assets/espaceco/pages/communities/CommunityListItem.tsx b/assets/espaceco/pages/communities/CommunityListItem.tsx new file mode 100644 index 00000000..6d76f400 --- /dev/null +++ b/assets/espaceco/pages/communities/CommunityListItem.tsx @@ -0,0 +1,62 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { FC } from "react"; +import { CommunityResponseDTO } from "../../../@types/espaceco"; +import useToggle from "../../../hooks/useToggle"; +import { useTranslation } from "../../../i18n/i18n"; + +import placeholder1x1 from "../../../img/placeholder.1x1.png"; + +import "../../../sass/pages/espaceco/community.scss"; +import { cx } from "@codegouvfr/react-dsfr/tools/cx"; + +type CommunityListItemProps = { + className?: string; + community: CommunityResponseDTO; +}; +const CommunityListItem: FC = ({ className, community }) => { + const { t } = useTranslation("EspaceCoCommunities"); + + const [showDescription, toggleShowDescription] = useToggle(false); + + /*const dataUsesQuery = useQuery({ + queryKey: RQKeys.datastore_stored_data_uses(datastoreId, vectorDb._id), + queryFn: ({ signal }) => api.storedData.getUses(datastoreId, vectorDb._id, { signal }), + staleTime: 600000, + }); */ + + return ( + <> +
+
+
+
+
+
+
+
+
+ {community.detailed_description &&
} + + ); +}; + +export default CommunityListItem; diff --git a/assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts b/assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts new file mode 100644 index 00000000..0232156c --- /dev/null +++ b/assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts @@ -0,0 +1,50 @@ +import { declareComponentKeys } from "i18nifty"; +import { Translations } from "../../../i18n/i18n"; +import { CommunityListFilter } from "../../../@types/app_espaceco"; + +// traductions +export const { i18n } = declareComponentKeys< + | "title" + | "filters" + | "all_public_communities" + | "communities_as_member" + | "pending_membership" + | { K: "no_result"; P: { filter: CommunityListFilter }; R: string } + | "search_placeholder" + | "no_options" + | "loading" + | "show_details" +>()("EspaceCoCommunities"); + +export const EspaceCoCommunitiesFrTranslations: Translations<"fr">["EspaceCoCommunities"] = { + title: "Liste des guichets", + filters: "Filtres", + all_public_communities: "Tous les guichets publics", + communities_as_member: "Guichets dont je suis membre", + pending_membership: "Adhésions en cours", + no_result: ({ filter }) => + filter === "public" + ? "Aucun guichet public" + : filter === "iam_member" + ? "Vous n'êtes membre d'aucun guichet" + : filter === "affiliation" + ? "Aucune affiliation en cours" + : "[TODO]", + search_placeholder: "Recherche d'un guichet par son nom", + no_options: "Aucun guichet", + loading: "Recherche en cours ...", + show_details: "Afficher les détails", +}; + +export const EspaceCoCommunitiesEnTranslations: Translations<"en">["EspaceCoCommunities"] = { + title: "List of communities", + filters: "Filters", + all_public_communities: undefined, + communities_as_member: undefined, + pending_membership: undefined, + no_result: ({ filter }) => `[TODO] ${filter}`, + search_placeholder: undefined, + no_options: undefined, + loading: undefined, + show_details: undefined, +}; diff --git a/assets/espaceco/pages/communities/SearchCommunity.tsx b/assets/espaceco/pages/communities/SearchCommunity.tsx new file mode 100644 index 00000000..6816badb --- /dev/null +++ b/assets/espaceco/pages/communities/SearchCommunity.tsx @@ -0,0 +1,55 @@ +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Autocomplete from "@mui/material/Autocomplete"; +import TextField from "@mui/material/TextField"; +import { useQuery } from "@tanstack/react-query"; +import { FC } from "react"; +import { useDebounceValue } from "usehooks-ts"; +import { CommunityListFilter } from "../../../@types/app_espaceco"; +import { CommunityResponseDTO } from "../../../@types/espaceco"; +import { useTranslation } from "../../../i18n/i18n"; +import RQKeys from "../../../modules/espaceco/RQKeys"; +import api from "../../api"; + +type SearchCommunityProps = { + filter: CommunityListFilter; + onChange: (value: CommunityResponseDTO | null) => void; +}; + +const SearchCommunity: FC = ({ filter, onChange }) => { + const { t } = useTranslation("EspaceCoCommunities"); + + const [search, setSearch] = useDebounceValue("", 500); + + const searchQuery = useQuery({ + queryKey: RQKeys.search(search, filter), + queryFn: ({ signal }) => api.community.searchByName(search, filter, signal), + enabled: search.length > 3, + }); + + return ( + + option.name} + options={searchQuery.data || []} + renderInput={(params) => ( + + )} + isOptionEqualToValue={(option, v) => option.id === v.id} + onInputChange={(_, v) => setSearch(v)} + onChange={(_, v) => onChange(v)} + /> + + ); +}; + +export default SearchCommunity; diff --git a/assets/i18n/i18n.ts b/assets/i18n/i18n.ts index 19dd2c13..d45fb696 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -44,7 +44,8 @@ export type ComponentKey = | typeof import("../entrepot/pages/service/wfs/WfsServiceForm").i18n | typeof import("../entrepot/pages/service/tms/PyramidVectorTmsServiceForm").i18n | typeof import("../entrepot/pages/service/TableSelection").i18n - | typeof import("../entrepot/pages/service/wms-vector/UploadStyleFile").i18n; + | typeof import("../entrepot/pages/service/wms-vector/UploadStyleFile").i18n + | typeof import("../espaceco/pages/communities/EspaceCoCommunitiesTr").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 0f549a93..86fac9f4 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -28,6 +28,7 @@ import { commonEnTranslations } from "../Common"; import { RightsEnTranslations } from "../Rights"; import { StyleEnTranslations } from "../Style"; import type { Translations } from "../i18n"; +import { EspaceCoCommunitiesEnTranslations } from "../../espaceco/pages/communities/EspaceCoCommunitiesTr"; export const translations: Translations<"en"> = { Common: commonEnTranslations, @@ -59,4 +60,5 @@ export const translations: Translations<"en"> = { TableSelection: TableSelectionEnTranslations, UploadStyleFile: UploadStyleFileEnTranslations, PyramidVectorTmsServiceForm: PyramidVectorTmsServiceFormEnTranslations, + EspaceCoCommunities: EspaceCoCommunitiesEnTranslations, }; diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index a5f1db67..334507a3 100644 --- a/assets/i18n/languages/fr.tsx +++ b/assets/i18n/languages/fr.tsx @@ -28,6 +28,7 @@ import { commonFrTranslations } from "../Common"; import { RightsFrTranslations } from "../Rights"; import { StyleFrTranslations } from "../Style"; import type { Translations } from "../i18n"; +import { EspaceCoCommunitiesFrTranslations } from "../../espaceco/pages/communities/EspaceCoCommunitiesTr"; export const translations: Translations<"fr"> = { Common: commonFrTranslations, @@ -59,4 +60,5 @@ export const translations: Translations<"fr"> = { TableSelection: TableSelectionFrTranslations, UploadStyleFile: UploadStyleFileFrTranslations, PyramidVectorTmsServiceForm: PyramidVectorTmsServiceFormFrTranslations, + EspaceCoCommunities: EspaceCoCommunitiesFrTranslations, }; diff --git a/assets/modules/espaceco/RQKeys.ts b/assets/modules/espaceco/RQKeys.ts index 3661d6fe..e602d0bf 100644 --- a/assets/modules/espaceco/RQKeys.ts +++ b/assets/modules/espaceco/RQKeys.ts @@ -1,5 +1,16 @@ +import { CommunityListFilter } from "../../@types/app_espaceco"; + const RQKeys = { - community_list: (name: string = "", page: number = 1, limit: number = 10): string[] => ["community", name, page.toString(), limit.toString()], + community_list: (page: number, limit: number): string[] => ["community", page.toString(), limit.toString()], + search: (search: string, filter: CommunityListFilter): string[] => { + return ["search", "community", search, filter]; + }, + communities_as_member: (pending: boolean, page: number, limit: number): string[] => [ + "communities_as_member", + new Boolean(pending).toString(), + page.toString(), + limit.toString(), + ], }; export default RQKeys; diff --git a/assets/router/RouterRenderer.tsx b/assets/router/RouterRenderer.tsx index a88abeb3..81af10a7 100644 --- a/assets/router/RouterRenderer.tsx +++ b/assets/router/RouterRenderer.tsx @@ -52,7 +52,7 @@ const PyramidVectorTmsServiceForm = lazy(() => import("../entrepot/pages/service const ServiceView = lazy(() => import("../entrepot/pages/service/view/ServiceView")); -const EspaceCoCommunityList = lazy(() => import("../espaceco/pages/communities/CommunityList")); +const EspaceCoCommunityList = lazy(() => import("../espaceco/pages/communities/Communities")); const RouterRenderer: FC = () => { const route = useRoute(); diff --git a/assets/sass/pages/espaceco/community.scss b/assets/sass/pages/espaceco/community.scss new file mode 100644 index 00000000..d573fc4e --- /dev/null +++ b/assets/sass/pages/espaceco/community.scss @@ -0,0 +1,3 @@ +.frx-community-even { + background-color: var(--background-alt-grey); +} diff --git a/package.json b/package.json index 8d6100a3..61a84b6c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "simple-zustand-devtools": "^1.1.0", "tsafe": "^1.6.5", "type-route": "^1.0.1", + "usehooks-ts": "^3.1.0", "uuid": "^9.0.0", "validator": "^13.11.0", "yup": "^1.2.0", @@ -86,4 +87,4 @@ "browserslist": [ "defaults" ] -} \ No newline at end of file +} diff --git a/src/Controller/EspaceCo/CommunityController.php b/src/Controller/EspaceCo/CommunityController.php index e1f06ee2..10525978 100644 --- a/src/Controller/EspaceCo/CommunityController.php +++ b/src/Controller/EspaceCo/CommunityController.php @@ -7,6 +7,7 @@ use App\Controller\ApiControllerInterface; use Symfony\Component\Routing\Attribute\Route; use App\Services\EspaceCoApi\CommunityApiService; +use App\Services\EspaceCoApi\UserApiService; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -19,22 +20,139 @@ )] class CommunityController extends AbstractController implements ApiControllerInterface { + const SEARCH_LIMIT = 20; + public function __construct( private CommunityApiService $communityApiService, + private UserApiService $userApiService ) { } #[Route('/get', name: 'get', methods: ['GET'])] public function get( + #[MapQueryParameter] ?string $name = '', + #[MapQueryParameter] ?int $page = 1, + #[MapQueryParameter] ?int $limit = 10, + #[MapQueryParameter] ?string $sort = 'name:DESC', + ): JsonResponse + { + try { + $response = $this->communityApiService->getCommunities($name, $page, $limit, $sort); + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/get_as_member', name: 'get_as_member', methods: ['GET'])] + public function getMeMember( + #[MapQueryParameter] bool $pending, #[MapQueryParameter] ?int $page = 1, #[MapQueryParameter] ?int $limit = 10 ): JsonResponse { try { - $response = $this->communityApiService->get($page, $limit); + $me = $this->userApiService->getMe(); + $members = array_map(function ($member) { + return ['id'=> $member['community_id'], 'name' => $member['community_name'], 'role' => $member['role']]; + },$me['communities_member']); + + $members = array_filter($members, function($member) use ($pending) { + return $pending ? $member['role'] === 'pending' : $member['role'] !== 'pending'; + }); + + $this->_sortMembersByName($members); + $members = array_slice(array_values($members), ($page - 1) * $limit, $limit); + + $communities = []; + foreach($members as $member) { + $community = $this->communityApiService->getCommunity($member['id']); + $communities[] = $community; + } + + $totalPages = floor(count($members) / $limit) + 1; + $previousPage = $page === 1 ? null : $page - 1; + $nextPage = $page + 1 > $totalPages ? null : $page + 1; + + return new JsonResponse([ + 'content' => $communities, + 'totalPages' => $totalPages, + 'previousPage' => $previousPage, + 'nextPage' => $nextPage, + ]); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/search', name: 'search', methods: ['GET'])] + public function search( + #[MapQueryParameter] string $name, + #[MapQueryParameter] string $filter, + #[MapQueryParameter] string $sort, + ): JsonResponse + { + try { + if (! in_array($filter, ['public','iam_member','affiliation'])) { + throw new ApiException("Le filtre doit être public, iam_member ou affiliation"); + } + + if ($filter == 'public') { + $result = $this->communityApiService->getCommunities($name, 1, self::SEARCH_LIMIT, $sort); + $response = $result['content']; + } else { + $response = $this->_search($name, $filter !== 'iam_member' ); + } return new JsonResponse($response); } catch (ApiException $ex) { throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); } } + + /** + * @param string $name + * @param boolean $pending + * @return array + */ + private function _search(string $name, bool $pending) : array { + $me = $this->userApiService->getMe(); + $members = array_map(function ($member) { + return ['id'=> $member['community_id'], 'name' => $member['community_name'], 'role' => $member['role']]; + },$me['communities_member']); + + $regex = mb_strtoupper(str_replace('%', '', $name)); + $members = array_filter($members, function($member) use ($regex, $pending) { + $communityName = mb_strtoupper($member['name']); + $match = preg_match("/$regex/", $communityName); + return $pending ? ($member['role'] === 'pending' && $match) : ($member['role'] !== 'pending' && $match); + }); + + $this->_sortMembersByName($members); + $members = array_slice(array_values($members), 0, self::SEARCH_LIMIT); + + $communities = []; + foreach($members as $member) { + $community = $this->communityApiService->getCommunity($member['id']); + $communities[] = $community; + } + + return $communities; + } + + /** + * @param array $members + * @return void + */ + private function _sortMembersByName(array &$members) : void + { + usort($members, function($m1, $m2) { + $upM1 = mb_strtoupper($m1['name']); + $upM2 = mb_strtoupper($m2['name']); + + if ($upM1 == $upM2) { + return 0; + } + return ($upM1 < $upM2) ? -1 : 1; + }); + } } diff --git a/src/Controller/EspaceCo/UserController.php b/src/Controller/EspaceCo/UserController.php new file mode 100644 index 00000000..60867501 --- /dev/null +++ b/src/Controller/EspaceCo/UserController.php @@ -0,0 +1,30 @@ + true], + condition: 'request.isXmlHttpRequest()' +)] +class UserController extends AbstractController implements ApiControllerInterface +{ + public function __construct( + private UserApiService $userApiService, + ) { + } + + #[Route('/me', name: 'me')] + public function getCurrentUser(): JsonResponse + { + $me = $this->userApiService->getMe(); + return $this->json($me); + } +} \ No newline at end of file diff --git a/src/Services/EspaceCoApi/CommunityApiService.php b/src/Services/EspaceCoApi/CommunityApiService.php index f3cf670c..ff622e81 100644 --- a/src/Services/EspaceCoApi/CommunityApiService.php +++ b/src/Services/EspaceCoApi/CommunityApiService.php @@ -4,9 +4,9 @@ class CommunityApiService extends BaseEspaceCoApiService { - public function get(int $page = 1,int $limit = 10): array + public function getCommunities(string $name, int $page,int $limit, string $sort): array { - $response = $this->request('GET', "communities", [], ['page' => $page, 'limit' => $limit], [], false, true, true); + $response = $this->request('GET', "communities", [], ['name' => $name, 'page' => $page, 'limit' => $limit, 'sort' => $sort], [], false, true, true); $contentRange = $response['headers']['content-range'][0]; $totalPages = $this->getResultsPageCount($contentRange, $limit); @@ -21,4 +21,12 @@ public function get(int $page = 1,int $limit = 10): array 'nextPage' => $nextPage, ]; } + + /** + * @param string $communityId + * @return array + */ + public function getCommunity(string $communityId) : array { + return $this->request('GET', "communities/$communityId"); + } } \ No newline at end of file diff --git a/src/Services/EspaceCoApi/UserApiService.php b/src/Services/EspaceCoApi/UserApiService.php new file mode 100644 index 00000000..7d8c6009 --- /dev/null +++ b/src/Services/EspaceCoApi/UserApiService.php @@ -0,0 +1,11 @@ +request('GET', 'users/me'); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1aa9e5d7..c7fb1ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8828,6 +8828,13 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +usehooks-ts@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.0.tgz#156119f36efc85f1b1952616c02580f140950eca" + integrity sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw== + dependencies: + lodash.debounce "^4.0.8" + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"