diff --git a/src/WrappedRender.tsx b/src/WrappedRender.tsx
index cbb9c43..873a7f5 100644
--- a/src/WrappedRender.tsx
+++ b/src/WrappedRender.tsx
@@ -5,15 +5,31 @@ import { messagesFr } from "./i18n-fr";
import { IntlProvider } from "react-intl";
import { ReactElement } from "react";
import { SonorTheme } from "./theme";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { AuthProvider } from "./hooks/useAuth";
const router = createBrowserRouter(routes);
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000,
+ refetchOnWindowFocus: false,
+ retry: false,
+ },
+ },
+});
+
export const WrappedRender = (children: ReactElement) => {
return render(
-
-
- {children}
-
- ,
+
+
+
+
+ {children}
+
+
+
+ ,
);
};
diff --git a/src/functions/api.ts b/src/functions/api.ts
index 3159ee3..431c799 100644
--- a/src/functions/api.ts
+++ b/src/functions/api.ts
@@ -1,7 +1,20 @@
-import type { APIPaths, APIRequests, APIResponse } from "../types/api.ts";
+import type { APIPaths, APIRequests, APIResponse, APIMethods, APIRequest } from "../types/api.ts";
const baseURL = import.meta.env.VITE_API_ENDPOINT;
+const mockApiRequest = new Map any>()
+
+export function mockRequest<
+Path extends APIPaths,
+Method extends APIMethods,
+>(path: Path, method: Method, cb: (options: APIRequest) => APIResponse) {
+ mockApiRequest.set(`${method} ${path}`, cb as any)
+}
+
+export function clearMockRequest() {
+ mockApiRequest.clear()
+}
+
export async function fetchAPI<
Path extends APIPaths,
Options extends APIRequests & { signal?: AbortSignal; headers?: Record },
@@ -46,6 +59,10 @@ export async function fetchAPI<
}
}
}
+ const requestKey = `${fetchOptions.method?.toLowerCase()} ${path}`
+ if (mockApiRequest.has(requestKey)) {
+ return (mockApiRequest.get('requestKey') as any)(options) as APIResponse
+ }
const response = await fetch(url.toString(), fetchOptions);
if (response.status === 204) {
return null as any;
diff --git a/src/functions/oidc.ts b/src/functions/oidc.ts
index 5e56ec0..01a06a9 100644
--- a/src/functions/oidc.ts
+++ b/src/functions/oidc.ts
@@ -26,7 +26,8 @@ export const createAppOidc = () => {
if (isOidc) {
return !import.meta.env.VITE_OIDC_ISSUER
? createMockReactOidc({
- isUserInitiallyLoggedIn: false,
+ isUserInitiallyLoggedIn: true,
+
mockedTokens: {
decodedIdToken: {
inseegroupedefaut: ["gr"],
diff --git a/src/i18n-en.js b/src/i18n-en.js
index 1838e96..a2df1ef 100644
--- a/src/i18n-en.js
+++ b/src/i18n-en.js
@@ -21,4 +21,26 @@ export const messagesEn = {
statesFilterLabel: 'States...',
closingCauseFilterLabel: 'Closing cause...',
priorityFilterLabel: 'Priority...',
+ searchInterviewer: 'an interviewer...',
+ searchOrganizationUnit: "an organization unit...",
+ chooseSurvey: "Choose a survey :",
+ chooseInterviewer: "Choose an interviewer :",
+ chooseOrganizationUnit: "Choose an organization unit :",
+ followInterviewer: "Follow an interviewer",
+ followSurvey: 'Follow a survey',
+ allSurveys: 'All surveys',
+ followOrganizationUnit: "Follow an organization unit",
+ followCampaignBreacrumb: "Follow survey",
+ survey: "Survey",
+ progress: "Progress",
+ collect: "Collect",
+ reminders: "Reminders",
+ provisionalStatus: "Provisional status",
+ closedSU: "Closed SU",
+ terminatedSU: "Terminated SU",
+ interviewer: "Interviewer",
+ followInterviewerBreacrumb: "Follow interviewer",
+ lastSynchronization: "Last synchronization",
+ followOrganizationUnitBreacrumb: "Follow organization unit",
+ organizationUnit: "OU"
}
\ No newline at end of file
diff --git a/src/i18n-fr.js b/src/i18n-fr.js
index 610899a..848ed23 100644
--- a/src/i18n-fr.js
+++ b/src/i18n-fr.js
@@ -21,4 +21,28 @@ export const messagesFr = {
statesFilterLabel: 'Etat...',
closingCauseFilterLabel: 'Bilan agrégé...',
priorityFilterLabel: 'Prioritaire...',
+ searchInterviewer: 'un enquêteur...',
+ searchOrganizationUnit: "un site...",
+ chooseSurvey: "Choisissez une enquête :",
+ chooseInterviewer: "Choisissez un enquêteur / une enquêtrice :",
+ chooseOrganizationUnit: "Choisissez un site :",
+ followInterviewer: "Suivre un enquêteur",
+ followSurvey: "Suivre une enquête",
+ allSurveys: "Ensemble des enquêtes",
+ followOrganizationUnit: "Suivre un site",
+ followCampaignBreacrumb: "Suivre enquête",
+ survey: "Enquête",
+ progress: "Avancement",
+ collect: "Collecte",
+ reminders: "Relances",
+ provisionalStatus: "Statut provisoire",
+ closedSU: "UE cloturées",
+ terminatedSU: "UE terminées",
+ interviewer: "Enquêteur",
+ followInterviewerBreacrumb: "Suivre enquêteur",
+ lastSynchronization: "Dernière synchronization",
+ followOrganizationUnitBreacrumb: "Suivre site",
+ organizationUnit: "DEM"
+
+
}
\ No newline at end of file
diff --git a/src/pages/FollowCampaignPage.tsx b/src/pages/FollowCampaignPage.tsx
new file mode 100644
index 0000000..554b363
--- /dev/null
+++ b/src/pages/FollowCampaignPage.tsx
@@ -0,0 +1,58 @@
+import { useIntl } from "react-intl";
+import { FollowSinglePageHeader } from "../ui/follow/FollowSinglePageHeader";
+import { SyntheticEvent, useState } from "react";
+import Tabs from "@mui/material/Tabs";
+import Stack from "@mui/material/Stack";
+import { PageTab } from "../ui/PageTab";
+import { FollowCampaignProgress } from "../ui/follow/FollowCampaignProgress";
+
+// TODO remove
+const labelMock = "Logement";
+
+enum Tab {
+ progress = "progress",
+ collect = "collect",
+ reminders = "reminders",
+ provisionalStatus = "provisionalStatus",
+ closedSU = "closedSU",
+ terminatedSU = "terminatedSU",
+}
+
+export const FollowCampaignPage = () => {
+ const intl = useIntl();
+ const [currentTab, setCurrentTab] = useState(Tab.progress);
+ const handleChange = (_: SyntheticEvent, newValue: string) => {
+ setCurrentTab(newValue);
+ };
+
+ const breadcrumbs = [
+ { href: "/follow", title: "goToFollowPage" },
+ intl.formatMessage({ id: "followCampaignBreacrumb" }),
+ labelMock,
+ ];
+
+ return (
+ <>
+
+ {Object.keys(Tab).map(k => (
+
+ ))}
+
+
+
+ {currentTab === Tab.progress && }
+ {currentTab === Tab.collect && <>tab collecte>}
+ {currentTab === Tab.reminders && <>tab relances>}
+ {currentTab === Tab.provisionalStatus && <>tab statut provisoire>}
+ {currentTab === Tab.closedSU && <>tab UE cloturées>}
+ {currentTab === Tab.terminatedSU && <>tab UE terminées>}
+
+ >
+ );
+};
diff --git a/src/pages/FollowInterviewerPage.tsx b/src/pages/FollowInterviewerPage.tsx
new file mode 100644
index 0000000..00bf1be
--- /dev/null
+++ b/src/pages/FollowInterviewerPage.tsx
@@ -0,0 +1,69 @@
+import { CircularProgress, Stack, Tabs } from "@mui/material";
+import { SyntheticEvent, useState } from "react";
+import { useIntl } from "react-intl";
+import { FollowSinglePageHeader } from "../ui/follow/FollowSinglePageHeader";
+import { PageTab } from "../ui/PageTab";
+import { Row } from "../ui/Row";
+import { useFetchQuery } from "../hooks/useFetchQuery";
+import { useParams } from "react-router-dom";
+
+enum Tab {
+ progress = "progress",
+ collect = "collect",
+ lastSynchronization = "lastSynchronization",
+}
+
+export const FollowInterviewerPage = () => {
+ const { id } = useParams();
+ const intl = useIntl();
+
+ const [currentTab, setCurrentTab] = useState(Tab.progress);
+
+ const { data: interviewer } = useFetchQuery("/api/interviewer/{id}", {
+ urlParams: {
+ id: id!,
+ },
+ });
+
+ if (!interviewer) {
+ return (
+
+
+
+ );
+ }
+
+ const label = `${interviewer.lastName?.toLocaleUpperCase() ?? ""} ${interviewer.firstName ?? ""}`;
+
+ const handleChange = (_: SyntheticEvent, newValue: string) => {
+ setCurrentTab(newValue);
+ };
+
+ const breadcrumbs = [
+ { href: "/follow", title: "goToFollowPage" },
+ intl.formatMessage({ id: "followInterviewerBreacrumb" }),
+ label,
+ ];
+
+ return (
+ <>
+
+ {Object.keys(Tab).map(k => (
+
+ ))}
+
+
+
+ {currentTab === Tab.progress && <>tab avancement>}
+ {currentTab === Tab.collect && <>tab collecte>}
+ {currentTab === Tab.lastSynchronization && <>tab dernière synchro>}
+
+ >
+ );
+};
diff --git a/src/pages/FollowOrganizationUnitPage.tsx b/src/pages/FollowOrganizationUnitPage.tsx
new file mode 100644
index 0000000..eccee54
--- /dev/null
+++ b/src/pages/FollowOrganizationUnitPage.tsx
@@ -0,0 +1,49 @@
+import { SyntheticEvent, useState } from "react";
+import { useIntl } from "react-intl";
+import { FollowSinglePageHeader } from "../ui/follow/FollowSinglePageHeader";
+
+import { PageTab } from "../ui/PageTab";
+import Stack from "@mui/material/Stack";
+
+// TODO remove
+const labelMock = "Paris";
+
+enum Tab {
+ progress = "progress",
+ collect = "collect",
+}
+
+export const FollowOrganizationUnitPage = () => {
+ const intl = useIntl();
+ const [currentTab, setCurrentTab] = useState(Tab.progress);
+ const handleChange = (_: SyntheticEvent, newValue: string) => {
+ setCurrentTab(newValue);
+ };
+
+ const breadcrumbs = [
+ { href: "/follow", title: "goToFollowPage" },
+ intl.formatMessage({ id: "followOrganizationUnitBreacrumb" }),
+ labelMock,
+ ];
+
+ return (
+ <>
+
+ {Object.keys(Tab).map(k => (
+
+ ))}
+
+
+
+ {currentTab === Tab.progress && <>tab avancement>}
+ {currentTab === Tab.collect && <>tab collecte>}
+
+ >
+ );
+};
diff --git a/src/pages/FollowPage.tsx b/src/pages/FollowPage.tsx
index d7a5e3d..766e231 100644
--- a/src/pages/FollowPage.tsx
+++ b/src/pages/FollowPage.tsx
@@ -1,5 +1,31 @@
-import { Typography } from "@mui/material";
+import { Row } from "../ui/Row";
+import { FollowInterviewerCard } from "../ui/follow/FollowInterviewerCard";
+import { FollowSurveyCard } from "../ui/follow/FollowSurveyCard";
+import { FollowOrganizationUnitCard } from "../ui/follow/FollowOrganizationUnitCard";
export const FollowPage = () => {
- return Page suivre;
+ // TODO use real condition
+ const isNationalProfile = true;
+
+ const gridTemplateColumns = isNationalProfile
+ ? { gridTemplateColumns: "1fr 1fr 1fr" }
+ : { gridTemplateColumns: "1fr 1fr" };
+
+ return (
+
+
+
+ {isNationalProfile && }
+
+ );
};
diff --git a/src/routes.tsx b/src/routes.tsx
index f6d6255..92b6bb2 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -8,6 +8,9 @@ import { ClosePage } from "./pages/ClosePage";
import { NotifyPage } from "./pages/NotifyPage";
import { CollectOrganizationPage } from "./pages/CollectOrganizationPage";
import { ReassignmentPage } from "./pages/ReassignmentPage";
+import { FollowInterviewerPage } from "./pages/FollowInterviewerPage";
+import { FollowCampaignPage } from "./pages/FollowCampaignPage";
+import { FollowOrganizationUnitPage } from "./pages/FollowOrganizationUnitPage";
export const routes: RouteObject[] = [
{
@@ -21,6 +24,9 @@ export const routes: RouteObject[] = [
children: [
{ path: "", element: },
{ path: "follow", element: },
+ { path: "follow/interviewer/:id", element: },
+ { path: "follow/campaign/:id", element: },
+ { path: "follow/organization-unit/:id", element: },
{ path: "read", element: },
{ path: "close", element: },
{ path: "notify", element: },
diff --git a/src/theme.tsx b/src/theme.tsx
index 6826824..d2988eb 100644
--- a/src/theme.tsx
+++ b/src/theme.tsx
@@ -119,6 +119,7 @@ declare module "@mui/material/Paper" {
declare module "@mui/material/Tab" {
interface TabPropsClassesOverrides {
search: true;
+ cardTab: true;
}
}
@@ -148,6 +149,7 @@ const typography = {
headlineLarge: {
fontSize: 32,
lineHeight: "40px",
+ fontWeight: 400,
},
headlineMedium: {
fontSize: 28,
@@ -323,6 +325,48 @@ export const theme = createTheme({
},
},
},
+ {
+ props: { classes: "cardTab" },
+ style: {
+ padding: "10px 40px",
+ backgroundColor: "#EFEFEF",
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ zIndex: 1,
+ overflow: "visible",
+
+ "&.Mui-selected": {
+ zIndex: 3,
+ background: "white",
+ "&:hover": {
+ background: "white",
+ },
+
+ "&::before": {
+ content: "''",
+ position: "absolute",
+ bottom: 0,
+ width: 15,
+ height: 15,
+ background:
+ "#FFF radial-gradient(circle at top left, #EFEFEF 0px, #EFEFEF 70%, transparent 71%, transparent 100%) no-repeat",
+ left: -15,
+ zIndex: 5,
+ },
+ "&::after": {
+ content: "''",
+ position: "absolute",
+ bottom: 0,
+ width: 15,
+ height: 15,
+ background:
+ "#FFF radial-gradient(circle at top right, #EFEFEF 0px, #EFEFEF 70%, transparent 71%, transparent 100%) no-repeat",
+ right: -15,
+ zIndex: 5,
+ },
+ },
+ },
+ },
],
},
MuiPaper: {
diff --git a/src/ui/Breadcrumbs.tsx b/src/ui/Breadcrumbs.tsx
new file mode 100644
index 0000000..c3a653d
--- /dev/null
+++ b/src/ui/Breadcrumbs.tsx
@@ -0,0 +1,53 @@
+import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
+import Box from "@mui/material/Box";
+import Link from "@mui/material/Link";
+import { NavLink } from "react-router-dom";
+import { useIntl } from "react-intl";
+
+export type BreadcrumbsItem = { href: string; title: string } | string;
+
+type Props = {
+ items: BreadcrumbsItem[];
+};
+
+export function Breadcrumbs({ items }: Readonly) {
+ return (
+
+ {items.map(item => (
+
+ ))}
+
+ );
+}
+
+function getKey(item: BreadcrumbsItem) {
+ if (typeof item === "string") {
+ return item;
+ }
+ return item.href;
+}
+
+function BreadcrumbsItem({ item }: Readonly<{ item: BreadcrumbsItem }>) {
+ const intl = useIntl();
+ if (typeof item === "string") {
+ return (
+
+ {item}
+
+ );
+ }
+
+ return (
+
+ {intl.formatMessage({ id: item.title })}
+
+ );
+}
diff --git a/src/ui/LoadingCell.tsx b/src/ui/LoadingCell.tsx
new file mode 100644
index 0000000..cea8079
--- /dev/null
+++ b/src/ui/LoadingCell.tsx
@@ -0,0 +1,13 @@
+import CircularProgress from "@mui/material/CircularProgress";
+import TableCell from "@mui/material/TableCell";
+import TableRow from "@mui/material/TableRow";
+
+export const LoadingCell = ({ columnLength }: { columnLength: number }) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/ui/PageTab.tsx b/src/ui/PageTab.tsx
new file mode 100644
index 0000000..e9f24e3
--- /dev/null
+++ b/src/ui/PageTab.tsx
@@ -0,0 +1,21 @@
+import Tab, { TabProps } from "@mui/material/Tab";
+
+type Props = {
+ label: string;
+} & TabProps;
+
+export const PageTab = ({ label, ...props }: Props) => {
+ return (
+
+ );
+};
diff --git a/src/ui/follow/FollowCampaignProgress.tsx b/src/ui/follow/FollowCampaignProgress.tsx
new file mode 100644
index 0000000..8d8b4b6
--- /dev/null
+++ b/src/ui/follow/FollowCampaignProgress.tsx
@@ -0,0 +1,51 @@
+import { useIntl } from "react-intl";
+import { SyntheticEvent, useState } from "react";
+import Tabs from "@mui/material/Tabs";
+import Stack from "@mui/material/Stack";
+import { SurveyGlobalFollow } from "./SurveyGlobalFollow";
+import { Tab as MuiTab } from "@mui/material";
+
+// TODO change tabs
+enum Tab {
+ progress = "progress",
+ collect = "collect",
+ reminders = "reminders",
+ provisionalStatus = "provisionalStatus",
+ closedSU = "closedSU",
+}
+
+export const FollowCampaignProgress = () => {
+ const intl = useIntl();
+ const [currentTab, setCurrentTab] = useState(Tab.progress);
+ const handleChange = (_: SyntheticEvent, newValue: Tab) => {
+ setCurrentTab(newValue);
+ };
+
+ return (
+ <>
+
+ {Object.keys(Tab).map(k => (
+
+ ))}
+
+
+
+ {/* todo changes components and tabs */}
+ {currentTab === Tab.progress && }
+ {currentTab === Tab.collect && }
+ {currentTab === Tab.reminders && }
+ {currentTab === Tab.provisionalStatus && }
+ {currentTab === Tab.closedSU && }
+
+ >
+ );
+};
diff --git a/src/ui/follow/FollowCardHeader.tsx b/src/ui/follow/FollowCardHeader.tsx
new file mode 100644
index 0000000..3b796ff
--- /dev/null
+++ b/src/ui/follow/FollowCardHeader.tsx
@@ -0,0 +1,30 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useIntl } from "react-intl";
+import { SearchField } from "../SearchField";
+import { ChangeEvent } from "react";
+
+type Props = {
+ title: string;
+ placeholder: string;
+ onSearch: (e: ChangeEvent) => void;
+};
+
+export const FollowCardHeader = ({ title, placeholder, onSearch }: Props) => {
+ const intl = useIntl();
+
+ const handleChange = (e: ChangeEvent) => {
+ onSearch(e);
+ };
+
+ return (
+
+ {intl.formatMessage({ id: title })}
+
+
+ );
+};
diff --git a/src/ui/follow/FollowInterviewerCard.tsx b/src/ui/follow/FollowInterviewerCard.tsx
new file mode 100644
index 0000000..bd6b005
--- /dev/null
+++ b/src/ui/follow/FollowInterviewerCard.tsx
@@ -0,0 +1,129 @@
+import { useIntl } from "react-intl";
+import { useDebouncedState } from "../../hooks/useDebouncedState";
+import Card from "@mui/material/Card";
+import Stack from "@mui/material/Stack";
+import TableContainer from "@mui/material/TableContainer";
+import Table from "@mui/material/Table";
+import TableHead from "@mui/material/TableHead";
+import { TableBody, TableCell, TableRow } from "@mui/material";
+import { useState } from "react";
+import { APISchemas } from "../../types/api";
+import { Link as RouterLink } from "react-router-dom";
+import { Link } from "../Link";
+import { FollowCardHeader } from "./FollowCardHeader";
+import { useFetchQuery } from "../../hooks/useFetchQuery";
+
+import { LoadingCell } from "../LoadingCell";
+
+export const FollowInterviewerCard = () => {
+ const intl = useIntl();
+ const [search, setSearch] = useDebouncedState("", 500);
+ const [page, setPage] = useState(0);
+
+ const { data: interviewers, isLoading } = useFetchQuery("/api/interviewers", { method: "get" });
+
+ const handleChangePage = (_: React.MouseEvent | null, newPage: number) => {
+ setPage(newPage);
+ };
+
+ const filteredInterviewers = filterInterviewers({ interviewers: interviewers ?? [], search });
+
+ return (
+
+
+ setSearch(e.target.value)}
+ placeholder="searchInterviewer"
+ />
+
+
+
+
+
+ {intl.formatMessage({ id: "chooseInterviewer" })}
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ filteredInterviewers.map(interviewer => (
+
+
+
+ {interviewer.interviewerLastName?.toLocaleUpperCase()}{" "}
+ {interviewer.interviewerFirstName}
+
+
+
+ ))
+ )}
+
+ {/* TODO use TableFooter */}
+
+
+
+
+ );
+};
+
+type FilterInterviewerProps = {
+ interviewers: APISchemas["InterviewerDto"][];
+ search?: string;
+};
+
+/* filterInterviewers :
+ conditions enable to filter when user search by:
+ - firstName
+ - lastName
+ - firstName follow by lastName
+ - lastName follow by firstName
+*/
+const filterInterviewers = ({ interviewers, search }: FilterInterviewerProps) => {
+ if (search) {
+ interviewers = interviewers.filter(
+ item =>
+ item.interviewerFirstName?.toLowerCase().includes(search.toLowerCase()) ||
+ item.interviewerLastName?.toLowerCase().includes(search.toLowerCase()) ||
+ (item.interviewerFirstName &&
+ item.interviewerLastName &&
+ (item.interviewerFirstName
+ ?.toLowerCase()
+ .concat(item.interviewerLastName?.toLowerCase())
+ .includes(search.split(" ").join("").toLowerCase()) ||
+ item.interviewerLastName
+ ?.toLowerCase()
+ .concat(item.interviewerFirstName?.toLowerCase())
+ .includes(search.split(" ").join("").toLowerCase()))),
+ );
+ }
+
+ return interviewers.sort((i1, i2) => {
+ const nameI1 = getInterviewerName({
+ lastName: i1.interviewerLastName,
+ firstName: i1.interviewerFirstName,
+ });
+
+ const nameI2 = getInterviewerName({
+ lastName: i2.interviewerLastName,
+ firstName: i2.interviewerFirstName,
+ });
+
+ return nameI1.trim().localeCompare(nameI2.trim());
+ });
+};
+
+const getInterviewerName = ({ lastName, firstName }: { lastName?: string; firstName?: string }) => {
+ if (lastName) {
+ return firstName ? lastName.concat(firstName) : lastName;
+ } else {
+ return firstName ?? "";
+ }
+};
diff --git a/src/ui/follow/FollowOrganizationUnitCard.test.tsx b/src/ui/follow/FollowOrganizationUnitCard.test.tsx
new file mode 100644
index 0000000..e80c1b0
--- /dev/null
+++ b/src/ui/follow/FollowOrganizationUnitCard.test.tsx
@@ -0,0 +1,55 @@
+import userEvent from "@testing-library/user-event";
+import { WrappedRender } from "../../WrappedRender";
+import { FollowOrganizationUnitCard } from "./FollowOrganizationUnitCard";
+import { act, screen, waitFor } from "@testing-library/react";
+import { clearMockRequest, mockRequest } from "../../functions/api";
+
+describe("FollowOrganizationUnitCard component", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ mockRequest("/api/organization-units", "get", () => [
+ {
+ id: "DR75",
+ label: "Ile de France",
+ type: "LOCAL" as const,
+ users: [],
+ },
+ {
+ id: "DR51",
+ label: "Champagne-Ardenne",
+ type: "LOCAL" as const,
+ users: [],
+ },
+ ]);
+ });
+
+ afterEach(() => clearMockRequest());
+
+ it("should render FollowOrganizationUnitCard component", () => {
+ WrappedRender();
+
+ expect(screen.getByText("Suivre un site")).toBeInTheDocument();
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ expect(screen.getByText("Champagne-Ardenne")).toBeInTheDocument();
+
+ expect(screen.getByText("Ile de France")).toBeInTheDocument();
+ });
+
+ it("should display search result", async () => {
+ await act(async () => WrappedRender());
+
+ userEvent.type(screen.getByRole("textbox"), "france");
+
+ expect(screen.getByText("Ile de France")).toBeInTheDocument();
+ await waitFor(() => expect(screen.queryByText("Champagne-Ardenne")).not.toBeInTheDocument());
+ });
+
+ it("should display sorted OU", async () => {
+ await act(async () => WrappedRender());
+
+ const links = screen.getAllByRole("link");
+ expect(links[0]).toContainHTML("Champagne-Ardenne");
+ expect(links[1]).toContainHTML("Ile de France");
+ });
+});
diff --git a/src/ui/follow/FollowOrganizationUnitCard.tsx b/src/ui/follow/FollowOrganizationUnitCard.tsx
new file mode 100644
index 0000000..c27eb65
--- /dev/null
+++ b/src/ui/follow/FollowOrganizationUnitCard.tsx
@@ -0,0 +1,96 @@
+import { useIntl } from "react-intl";
+import { useDebouncedState } from "../../hooks/useDebouncedState";
+import Card from "@mui/material/Card";
+import Stack from "@mui/material/Stack";
+import TableContainer from "@mui/material/TableContainer";
+import Table from "@mui/material/Table";
+import TableHead from "@mui/material/TableHead";
+import { TableBody, TableCell, TableRow } from "@mui/material";
+import { useState } from "react";
+import { Link as RouterLink } from "react-router-dom";
+import { Link } from "../Link";
+import { FollowCardHeader } from "./FollowCardHeader";
+import { useFetchQuery } from "../../hooks/useFetchQuery";
+
+import { APISchemas } from "../../types/api";
+import { LoadingCell } from "../LoadingCell";
+
+export const FollowOrganizationUnitCard = () => {
+ const intl = useIntl();
+ const [search, setSearch] = useDebouncedState("", 500);
+ const [page, setPage] = useState(0);
+
+ const { data: organizationUnits, isLoading } = useFetchQuery("/api/organization-units", {
+ method: "get",
+ });
+
+ const handleChangePage = (_: React.MouseEvent | null, newPage: number) => {
+ setPage(newPage);
+ };
+
+ const filteredOU = filterOrganizationUnits({ organizationUnits: organizationUnits ?? [], search });
+
+ return (
+
+
+ setSearch(e.target.value)}
+ placeholder="searchOrganizationUnit"
+ />
+
+
+
+
+
+ {intl.formatMessage({ id: "chooseOrganizationUnit" })}
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ filteredOU.map(OU => (
+
+
+
+ {OU.label}
+
+
+
+ ))
+ )}
+
+ {/* TODO use TableFooter */}
+
+
+
+
+ );
+};
+
+type FilterOUProps = {
+ organizationUnits: APISchemas["OrganizationUnitContextDto"][];
+ search?: string;
+};
+
+const filterOrganizationUnits = ({ organizationUnits, search }: FilterOUProps) => {
+ if (search) {
+ organizationUnits = organizationUnits.filter(item =>
+ item.label?.toLowerCase().includes(search.toLowerCase()),
+ );
+ }
+
+ return organizationUnits.sort((ou1, ou2) => {
+ if (!ou1.label || !ou2.label) {
+ return 1;
+ }
+ return ou1.label.localeCompare(ou2.label);
+ });
+};
diff --git a/src/ui/follow/FollowSinglePageHeader.tsx b/src/ui/follow/FollowSinglePageHeader.tsx
new file mode 100644
index 0000000..c579fad
--- /dev/null
+++ b/src/ui/follow/FollowSinglePageHeader.tsx
@@ -0,0 +1,59 @@
+import { useNavigate } from "react-router-dom";
+import { Row } from "../Row";
+import Stack from "@mui/material/Stack";
+import { Box, Divider, IconButton, Tabs, Typography } from "@mui/material";
+import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
+import { Breadcrumbs, BreadcrumbsItem } from "../Breadcrumbs";
+import { useIntl } from "react-intl";
+import { ReactNode, SyntheticEvent } from "react";
+
+type Props = {
+ category: string;
+ label: string;
+ breadcrumbs: BreadcrumbsItem[];
+ currentTab: string;
+ onChange: (_: SyntheticEvent, newValue: string) => void;
+ children: ReactNode;
+};
+
+export const FollowSinglePageHeader = ({
+ category,
+ label,
+ breadcrumbs,
+ currentTab,
+ onChange,
+ children,
+}: Props) => {
+ const navigate = useNavigate();
+ const intl = useIntl();
+
+ return (
+
+
+
+
+ navigate(-1)}>
+
+
+
+ {intl.formatMessage({ id: category })} -
+
+ {label}
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+};
diff --git a/src/ui/follow/FollowSurveyCard.tsx b/src/ui/follow/FollowSurveyCard.tsx
new file mode 100644
index 0000000..d1d1504
--- /dev/null
+++ b/src/ui/follow/FollowSurveyCard.tsx
@@ -0,0 +1,54 @@
+import { useIntl } from "react-intl";
+import Card from "@mui/material/Card";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+import { Link } from "../Link";
+import { Link as RouterLink } from "react-router-dom";
+
+const surveysMock = [
+ {
+ id: "1",
+ label: "Logement",
+ },
+ {
+ id: "2",
+ label: "Autonomie",
+ },
+ {
+ id: "3",
+ label: "TIC",
+ },
+];
+
+export const FollowSurveyCard = () => {
+ const intl = useIntl();
+
+ surveysMock.sort((su1, su2) => su1.label.localeCompare(su2.label));
+
+ return (
+
+
+ {intl.formatMessage({ id: "followSurvey" })}
+
+
+ {intl.formatMessage({ id: "chooseSurvey" })}
+
+ {surveysMock.map(survey => (
+
+ {survey.label}
+
+ ))}
+
+ {intl.formatMessage({ id: "allSurveys" })}
+
+
+
+ );
+};
diff --git a/src/ui/follow/SurveyGlobalFollow.tsx b/src/ui/follow/SurveyGlobalFollow.tsx
new file mode 100644
index 0000000..a34ceab
--- /dev/null
+++ b/src/ui/follow/SurveyGlobalFollow.tsx
@@ -0,0 +1,11 @@
+import { Card, Typography } from "@mui/material";
+import { useIntl } from "react-intl";
+
+export const SurveyGlobalFollow = () => {
+ const intl = useIntl();
+ return (
+
+ {intl.formatMessage({ id: "followSurvey" })}
+
+ );
+};