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" })} + + ); +};