diff --git a/package.json b/package.json
index 6381441..a6906ed 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "sonor",
"private": true,
- "version": "2.0.33",
+ "version": "2.0.34",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/sonar-project.properties b/sonar-project.properties
index 1c13fea..16ddbdf 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,2 +1,3 @@
sonar.projectKey=InseeFr_Sonor
-sonar.organization=inseefr
\ No newline at end of file
+sonar.organization=inseefr
+sonar.exclusions=src/i18n-fr.js,src/i18n-en.js
\ No newline at end of file
diff --git a/src/constants/closingCauses.ts b/src/constants/closingCauses.ts
new file mode 100644
index 0000000..0809e0c
--- /dev/null
+++ b/src/constants/closingCauses.ts
@@ -0,0 +1 @@
+export const closingCausesEnum = ["NPA", "NPI", "NPX", "ROW"];
diff --git a/src/constants/contactOutcome.ts b/src/constants/contactOutcome.ts
new file mode 100644
index 0000000..10724f2
--- /dev/null
+++ b/src/constants/contactOutcome.ts
@@ -0,0 +1,13 @@
+export const contactOutcomeEnum = [
+ "INA",
+ "IMP",
+ "REF",
+ "ALA",
+ "UCD",
+ "UTR",
+ "DCD",
+ "NUH",
+ "DUK",
+ "DUU",
+ "NOA",
+];
diff --git a/src/constants/surveyUnitStates.ts b/src/constants/surveyUnitStates.ts
new file mode 100644
index 0000000..79fd316
--- /dev/null
+++ b/src/constants/surveyUnitStates.ts
@@ -0,0 +1,17 @@
+export const surveyUnitStatesEnum = [
+ "NVM",
+ "NNS",
+ "ANV",
+ "VIN",
+ "VIC",
+ "PRC",
+ "AOC",
+ "APS",
+ "INS",
+ "WFT",
+ "WFS",
+ "TBR",
+ "FIN",
+ "NVA",
+ "CLO",
+];
diff --git a/src/hooks/useSearchFilter.ts b/src/hooks/useSearchFilter.ts
index 61d64ba..b80676f 100644
--- a/src/hooks/useSearchFilter.ts
+++ b/src/hooks/useSearchFilter.ts
@@ -7,7 +7,7 @@ export type Filter = {
ssech: string[];
interviewer: string[];
states: string[];
- closingCause: string[];
+ result: string[];
priority: string[];
all: { name: string; value: string }[];
};
@@ -17,7 +17,7 @@ export const emptyFilter: Filter = {
ssech: [],
interviewer: [],
states: [],
- closingCause: [],
+ result: [],
priority: [],
all: [],
};
diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts
new file mode 100644
index 0000000..32a42f8
--- /dev/null
+++ b/src/hooks/useTranslation.ts
@@ -0,0 +1,11 @@
+import { useIntl } from "react-intl";
+
+export const useTranslation = () => {
+ const intl = useIntl();
+
+ return {
+ translate: (id: string) => {
+ return intl.formatMessage({ id });
+ },
+ };
+};
diff --git a/src/i18n-en.js b/src/i18n-en.js
index 1838e96..ef58d36 100644
--- a/src/i18n-en.js
+++ b/src/i18n-en.js
@@ -5,9 +5,9 @@ export const messagesEn = {
goToNotifyPage: 'Notify' ,
goToCollectOrganization: 'Organization of collections',
goToReassignment: 'Reassignment',
- goToHelp: 'HELP',
+ goToHelp: 'help',
logout: 'Logout',
- selectFavoriteSurveys: 'Select my favorite surveys',
+ selectFavoriteSurveys: 'my favorite surveys',
resetFilters: 'Reset filters',
searchLabel: 'Search',
searchInterviewerPlaceholder: 'lastname, firstname',
@@ -19,6 +19,62 @@ export const messagesEn = {
subSampleFilterLabel: 'Sub-sample...',
interviewerFilterLabel: 'Interviewer...',
statesFilterLabel: 'States...',
- closingCauseFilterLabel: 'Closing cause...',
+ resultFilterLabel: 'Result...',
priorityFilterLabel: 'Priority...',
+ id: 'Identifier',
+ survey: 'Survey',
+ subSample: 'Sub-sample',
+ interviewer: 'Interviewer',
+ state: 'State',
+ closingCause: 'Closing cause',
+ result: 'Result',
+ contactOutcome: 'Contact outcome',
+ priority: 'Priority',
+ actions: 'Actions',
+ yes: 'Yes',
+ no: 'No',
+ readQuestionnaire: 'Read questionnaire',
+ close: 'Close',
+ comment: 'Comment',
+ surveyUnitNumber: 'SU n.',
+ commentPlaceholder: 'Write a comment',
+ commentDialogHelpText: 'Edit the text directly in the input field and click “validate” to validate your changes.',
+ delete:'delete',
+ cancel: 'cancel',
+ closeButtonLabel: 'close',
+ validate: 'validate',
+ labelRowsPerPage: 'Rows per page:',
+ labelDisplayedRows: 'entities displayed',
+ on: 'on',
+ NVM: 'Not visible to management',
+ NNS: 'Not Assigned, not started',
+ ANV: 'Assigned Not Visible to the interviewer',
+ VIN: 'Visible to the interviewer and not clickable',
+ VIC: 'Visible to the interviewer and clickable',
+ PRC: 'Preparing contact',
+ AOC: 'At least one contact made',
+ APS: 'Appointment scheduled',
+ INS: 'Interview started',
+ WFT: 'Waiting for transmission',
+ WFS: 'Waiting for synchronization',
+ TBR: 'To be reviewed',
+ FIN: 'Finalized',
+ NVA: 'Not available to all',
+ CLO: 'Closed',
+ NPA: 'Not processed : interviewer absence',
+ NPI: 'Not processed : interviewer presence',
+ NPX: 'Not processed for exceptional reason',
+ ROW: 'Right of withdrawal',
+ INA: 'Interview accepted',
+ IMP: 'Impossible to reach',
+ REF: 'Refusal',
+ ALA: 'Already answered (other mode)',
+ UCD: 'Unusable Contact Data',
+ UTR: 'Unable To Respond',
+ DCD: 'Deceased',
+ NUH: 'No longer Used for Habitation',
+ DUK: 'Definitely unavailable for a known reason',
+ DUU: 'Definitely unavailable for an unknown reason',
+ NOA: 'Not applicable',
+ myProfile: 'My profile'
}
\ No newline at end of file
diff --git a/src/i18n-fr.js b/src/i18n-fr.js
index 610899a..201b7af 100644
--- a/src/i18n-fr.js
+++ b/src/i18n-fr.js
@@ -5,9 +5,9 @@ export const messagesFr = {
goToNotifyPage: 'Notifier',
goToCollectOrganization: 'Organisation des collectes',
goToReassignment: 'Réaffectation',
- goToHelp: 'AIDE',
+ goToHelp: 'aide',
logout: 'Se déconnecter',
- selectFavoriteSurveys: 'Sélectionner mes enquêtes favorites',
+ selectFavoriteSurveys: 'mes enquêtes favorites',
resetFilters: 'Réinitialiser les filtres',
searchLabel: 'Recherche',
searchInterviewerPlaceholder: 'nom, prénom',
@@ -19,6 +19,62 @@ export const messagesFr = {
subSampleFilterLabel: 'Sous-échantillon...',
interviewerFilterLabel: 'Enquêteur...',
statesFilterLabel: 'Etat...',
- closingCauseFilterLabel: 'Bilan agrégé...',
+ resultFilterLabel: 'Résultat...',
priorityFilterLabel: 'Prioritaire...',
+ id: 'Identifiant',
+ survey: 'Enquête',
+ subSample: 'Sous-éch.',
+ interviewer: 'Enquêteur',
+ state: 'Etat',
+ closingCause: 'Motif de clotûre',
+ result: 'Résultat',
+ contactOutcome: 'Bilan des contacts',
+ priority: 'Prioritaire',
+ actions: 'Actions',
+ yes: 'Oui',
+ no: 'Non',
+ readQuestionnaire: 'Relire le questionnaire',
+ close: 'Clôturer',
+ comment: 'Commentaire',
+ surveyUnitNumber: 'UE n°',
+ commentPlaceholder: 'Rédiger un commentaire',
+ commentDialogHelpText: 'Modifiez le texte directement dans le champ de saisie et cliquez sur “valider” pour valider vos changements.',
+ delete:'supprimer',
+ cancel: 'annuler',
+ closeButtonLabel: 'fermer',
+ validate: 'valider',
+ labelRowsPerPage: 'Lignes par page :',
+ labelDisplayedRows: 'entités affichées',
+ on: 'sur',
+ NVM: 'UE non visible gestionnaire',
+ NNS: 'Non affecté, non commencé',
+ ANV: 'Affectée, non visible enquêteur',
+ VIN: 'Visible enquêteur et non cliquable',
+ VIC: 'Visible enquêteur et cliquable',
+ PRC: 'Prise de contact en préparation',
+ AOC: 'Au moins un contact',
+ APS: 'Rendez-vous pris',
+ INS: 'Questionnaire démarré',
+ WFT: 'UE en attente de transmission',
+ WFS: 'UE en attente de synchronisation',
+ TBR: 'UE à relire DEM',
+ FIN: 'UE finalisée',
+ NVA: 'UE non visible gestionnaire',
+ CLO: 'Clôturée',
+ NPA: 'Non traité enquêteur absent',
+ NPI: 'Non traité enquêteur présent',
+ NPX: 'Non traité pour cause exceptionnelle',
+ ROW: 'Droit de retrait',
+ INA: 'Enquête acceptée',
+ IMP: 'Impossible à joindre',
+ REF: 'Refus',
+ ALA: 'Déjà répondu (autre mode)',
+ UCD: 'Données de contact inutilisables (tél, mail)',
+ UTR: 'Incapacité à répondre',
+ DCD: 'Enquêté décédé',
+ NUH: 'Logement ayant perdu son usage d\'habitation',
+ DUK: 'Indisponibilité définitive pour motif connu',
+ DUU: 'Indisponibilité définitive pour motif inconnu',
+ NOA: 'Sans objet',
+ myProfile: 'Mon profil'
}
\ No newline at end of file
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index db1a8a2..3b92bca 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -1,17 +1,19 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { FiltersCard } from "../ui/FiltersCard";
-import { useIntl } from "react-intl";
+import { HomeTableCard } from "../ui/HomeTableCard";
+import { useTranslation } from "../hooks/useTranslation";
export const Home = () => {
- const intl = useIntl();
+ const { translate } = useTranslation();
return (
- {intl.formatMessage({ id: "homepageTitle" })}
+ {translate("homepageTitle")}
+
);
};
diff --git a/src/pages/SurveyUnitPage.tsx b/src/pages/SurveyUnitPage.tsx
new file mode 100644
index 0000000..a72866b
--- /dev/null
+++ b/src/pages/SurveyUnitPage.tsx
@@ -0,0 +1,7 @@
+import { Stack } from "@mui/material";
+import { useParams } from "react-router-dom";
+
+export const SurveyUnitPage = () => {
+ const { id } = useParams();
+ return UE {id};
+};
diff --git a/src/routes.tsx b/src/routes.tsx
index f6d6255..e363a85 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -8,6 +8,7 @@ import { ClosePage } from "./pages/ClosePage";
import { NotifyPage } from "./pages/NotifyPage";
import { CollectOrganizationPage } from "./pages/CollectOrganizationPage";
import { ReassignmentPage } from "./pages/ReassignmentPage";
+import { SurveyUnitPage } from "./pages/SurveyUnitPage";
export const routes: RouteObject[] = [
{
@@ -26,6 +27,7 @@ export const routes: RouteObject[] = [
{ path: "notify", element: },
{ path: "collectOrganization", element: },
{ path: "reassignment", element: },
+ { path: "survey-unit/:id", element: },
],
},
];
diff --git a/src/theme.tsx b/src/theme.tsx
index 6826824..4e83c92 100644
--- a/src/theme.tsx
+++ b/src/theme.tsx
@@ -103,6 +103,7 @@ declare module "@mui/material/SvgIcon" {
tabTitle: true;
headerSinglePage: true;
cardTitle: true;
+ littleIcon: true;
}
interface SvgIconPropsColorOverrides {
yellow: true;
@@ -132,6 +133,9 @@ const baseTheme = createTheme({});
const colors = (c: string) => baseTheme.palette.augmentColor({ color: { main: c } });
const typography = {
fontFamily: "Open Sans, sans-serif",
+ fontWeight: {
+ standard: 400,
+ },
displayLarge: {
fontSize: 57,
lineHeight: "64px",
@@ -183,7 +187,7 @@ const typography = {
fontSize: 12,
lineHeight: "16px",
fontWeight: 600,
- letterSpacing: 0.5,
+ letterSpacing: 0.1,
},
labelSmall: {
fontSize: 11,
@@ -298,29 +302,10 @@ export const theme = createTheme({
fontSize: 24,
},
},
- ],
- },
- MuiTab: {
- variants: [
{
- props: { classes: "search" },
+ props: { fontSize: "littleIcon" },
style: {
- fontSize: "12px",
- fontWeight: 600,
- lineHeight: "16px",
- textTransform: "none",
- color: palette.text.tertiary,
- padding: 8,
- minHeight: 54,
- whiteSpace: "nowrap",
-
- "&.Mui-selected": {
- boxShadow: "0px 4px 4px 0px rgba(0, 0, 0, 0.25)",
- backgroundColor: palette.primary.light,
- },
- "&>.MuiTab-iconWrapper": {
- margin: 0,
- },
+ fontSize: 16,
},
},
],
@@ -346,43 +331,10 @@ export const theme = createTheme({
},
],
},
- MuiToggleButtonGroup: {
- styleOverrides: {
- root: {
- borderRadius: 24,
- boxShadow: `inset 0 0 0 1px ${palette.text.tertiary}`,
- ".MuiToggleButtonGroup-grouped.Mui-selected": {
- position: "relative",
- zIndex: 2,
- },
- ".MuiToggleButtonGroup-grouped:not(:first-of-type)": {
- marginLeft: -12,
- zIndex: 1,
- paddingLeft: 24,
- },
- },
- },
- },
MuiTableCell: {
styleOverrides: {
root: {
- borderBottom: "none",
- },
- },
- },
- MuiToggleButton: {
- styleOverrides: {
- root: {
- borderRadius: 24,
- paddingInline: 24,
- border: "none",
- ...typography.bodyMedium,
- textTransform: "none",
- "&.Mui-selected, &.Mui-selected:hover": {
- borderRadius: "24px!important",
- background: palette.primary.main,
- color: "white",
- },
+ borderBottom: `solid 1px ${palette.text.hint}`,
},
},
},
@@ -409,6 +361,7 @@ export const theme = createTheme({
...typography.titleMedium,
fontSize: "20px",
lineHeight: "32px",
+ fontWeight: typography.fontWeight.standard,
},
},
},
@@ -439,13 +392,9 @@ export const theme = createTheme({
styleOverrides: {
colorError: {
backgroundColor: "#FDDBC3",
- color: "#C71A01",
".MuiChip-deleteIcon": {
color: "#C71A01",
},
- "&:hover": {
- backgroundColor: "#F9C3AF",
- },
},
colorSuccess: {
backgroundColor: "#D0E6D4",
@@ -453,14 +402,16 @@ export const theme = createTheme({
".MuiChip-deleteIcon": {
color: "#057345",
},
- "&:hover": {
- backgroundColor: "#A9C6AF",
- },
},
root: {
+ height: "26px",
".MuiChip-deleteIcon": {
color: palette.text.primary,
},
+ ".MuiChip-label": {
+ ...typography.labelMedium,
+ color: palette.black.main,
+ },
},
},
},
diff --git a/src/types/temporaryTypes.ts b/src/types/temporaryTypes.ts
new file mode 100644
index 0000000..fa5b5f1
--- /dev/null
+++ b/src/types/temporaryTypes.ts
@@ -0,0 +1,13 @@
+export type SurveyUnitTemporaryType = {
+ id: string;
+ campaignLabel: string;
+ ssech: number;
+ interviewer: string;
+ states: string;
+ result: string;
+ contactOutcome: {
+ date: number;
+ type: string;
+ };
+ priority: boolean;
+};
diff --git a/src/ui/AccountNavigation.tsx b/src/ui/AccountNavigation.tsx
index 5662035..6f393b1 100644
--- a/src/ui/AccountNavigation.tsx
+++ b/src/ui/AccountNavigation.tsx
@@ -1,6 +1,6 @@
import MenuItem from "@mui/material/MenuItem";
import Box from "@mui/material/Box";
-import { Button, Menu, Typography } from "@mui/material";
+import { Button, Divider, Menu, Typography } from "@mui/material";
import { useState } from "react";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
@@ -9,10 +9,10 @@ import { useLogout, useUser } from "../hooks/useAuth";
import { Link } from "./Link";
import { Row } from "./Row";
import { theme } from "../theme";
-import { useIntl } from "react-intl";
+import { useTranslation } from "../hooks/useTranslation";
export const AccountNavigation = () => {
- const intl = useIntl();
+ const { translate } = useTranslation();
const { name } = useUser();
const logout = useLogout();
@@ -36,7 +36,7 @@ export const AccountNavigation = () => {
startIcon={}
endIcon={}
>
- {name}
+ {translate("myProfile")}
diff --git a/src/ui/CommentDialog.test.tsx b/src/ui/CommentDialog.test.tsx
new file mode 100644
index 0000000..d71053e
--- /dev/null
+++ b/src/ui/CommentDialog.test.tsx
@@ -0,0 +1,98 @@
+import { fireEvent, screen, waitFor } from "@testing-library/react";
+import { WrappedRender } from "../WrappedRender";
+import { CommentDialog } from "./CommentDialog";
+import userEvent from "@testing-library/user-event";
+import * as hooks from "../hooks/useFetchQuery";
+import { UseMutationResult } from "@tanstack/react-query";
+import { APIEndpoints, APIMethods, APIRequest } from "../types/api";
+import { APIError } from "../functions/api";
+import { beforeEach } from "vitest";
+
+describe("CommentDialog component", () => {
+ const onCloseMock = vi.fn();
+ const surveyUnitId = "suId";
+ const mockMutationFunction = vi.fn();
+
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+
+ const mockedResponse = {
+ mutateAsync: mockMutationFunction,
+ mutationFn: vi.fn(),
+ } as unknown as UseMutationResult<
+ never,
+ APIError,
+ Omit>, "method">,
+ unknown
+ >;
+
+ vi.spyOn(hooks, "useFetchMutation").mockReturnValue(mockedResponse);
+ });
+
+ it("should render CommentDialog without comment and call onClose", () => {
+ WrappedRender();
+
+ expect(screen.getByText(/ue n° suid/i)).toBeInTheDocument();
+ expect(screen.getByText("valider")).toBeInTheDocument();
+ expect(screen.getByText("valider")).toBeDisabled();
+ expect(screen.getByText("annuler")).toBeInTheDocument();
+ fireEvent.click(screen.getByText("annuler"));
+ expect(onCloseMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("should render CommentDialog without comment and change validate button disability", async () => {
+ WrappedRender();
+ const validateButton = screen.getByText("valider");
+ expect(validateButton).toBeDisabled();
+ await waitFor(() => userEvent.type(screen.getByRole("textbox"), "comment"));
+ expect(validateButton).not.toBeDisabled();
+ await waitFor(() => userEvent.click(validateButton));
+ expect(mockMutationFunction).toHaveBeenCalledWith({
+ body: {
+ "type": "MANAGEMENT",
+ "value": "comment",
+ },
+ urlParams: {
+ "id": "suId",
+ },
+ });
+ expect(onCloseMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("should render CommentDialog with a comment", async () => {
+ WrappedRender(
+ ,
+ );
+
+ expect(screen.getByText(/ue n° suid/i)).toBeInTheDocument();
+ expect(screen.getByText("fermer")).toBeInTheDocument();
+ expect(screen.getByText("supprimer")).toBeInTheDocument();
+ expect(screen.getByText("comment")).toBeInTheDocument();
+
+ await waitFor(() => userEvent.type(screen.getByRole("textbox"), " text"));
+ expect(screen.getByText("comment text")).toBeInTheDocument();
+ expect(screen.getByText("valider")).toBeInTheDocument();
+ expect(screen.getByText("annuler")).toBeInTheDocument();
+ });
+
+ it("should render CommentDialog with a comment and close dialog on delete", async () => {
+ WrappedRender(
+ ,
+ );
+
+ const deleteButton = screen.getByText("supprimer");
+ expect(deleteButton).toBeInTheDocument();
+ await waitFor(() => userEvent.click(deleteButton));
+ expect(mockMutationFunction).toHaveBeenCalledWith({
+ body: {
+ "type": "MANAGEMENT",
+ "value": "",
+ },
+ urlParams: {
+ "id": "suId",
+ },
+ });
+ expect(onCloseMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/ui/CommentDialog.tsx b/src/ui/CommentDialog.tsx
new file mode 100644
index 0000000..d945824
--- /dev/null
+++ b/src/ui/CommentDialog.tsx
@@ -0,0 +1,131 @@
+import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import TextField from "@mui/material/TextField";
+import { theme } from "../theme";
+import { ChangeEvent, useState } from "react";
+import { Box, Divider, InputAdornment, Stack, Typography } from "@mui/material";
+import { Row } from "./Row";
+import { useFetchMutation } from "../hooks/useFetchQuery";
+import { useTranslation } from "../hooks/useTranslation";
+type Props = {
+ open: boolean;
+ onClose: () => void;
+ comment?: string;
+ surveyUnitId: string;
+};
+
+export const CommentDialog = ({ open, onClose, comment = "", surveyUnitId }: Props) => {
+ const { translate } = useTranslation();
+ const [newComment, setNewComment] = useState(comment);
+
+ const { mutateAsync, isPending } = useFetchMutation("/api/survey-unit/{id}/comment", "put");
+
+ const onChange = (event: ChangeEvent) => {
+ const value = event.target.value;
+ value !== newComment && setNewComment(value);
+ };
+
+ const handleCancel = () => {
+ onClose();
+ setNewComment(comment);
+ };
+
+ const handleDelete = async () => {
+ await mutateAsync({
+ body: { type: "MANAGEMENT", value: "" },
+ urlParams: { id: surveyUnitId },
+ });
+ onClose();
+ };
+
+ const handleSubmit: React.FormEventHandler = async e => {
+ e.preventDefault();
+ const data = new FormData(e.currentTarget);
+ const commentData = data.get("comment") as string;
+ await mutateAsync({
+ body: { type: "MANAGEMENT", value: commentData },
+ urlParams: { id: surveyUnitId },
+ });
+
+ onClose();
+ };
+
+ const isModified = newComment !== comment;
+
+ return (
+
+ );
+};
diff --git a/src/ui/FiltersCard.tsx b/src/ui/FiltersCard.tsx
index 32aa20d..d8c702c 100644
--- a/src/ui/FiltersCard.tsx
+++ b/src/ui/FiltersCard.tsx
@@ -7,7 +7,8 @@ import { SelectWithCheckbox, Option } from "./SelectWithCheckbox";
import ClearIcon from "@mui/icons-material/Clear";
import { useGetSearchFilter, useSearchForm, useToggleSearchFilter } from "../hooks/useSearchFilter";
import Chip from "@mui/material/Chip";
-import { useIntl } from "react-intl";
+import { surveyUnitStatesEnum } from "../constants/surveyUnitStates";
+import { useTranslation } from "../hooks/useTranslation";
const styles = {
Grid: {
@@ -17,13 +18,7 @@ const styles = {
},
};
-// TODO: add other options (campaigns, states, ssech ...)
-const closingCauseOptions = [
- { label: "Enquête acceptée", value: "ACCEPTED" },
- { label: "Déchet", value: "WASTE" },
- { label: "Hors champ", value: "HC" },
-];
-
+// TODO: add real options
const priorityOptions = [
{ label: "Oui", value: "true" },
{ label: "Non", value: "false" },
@@ -36,40 +31,76 @@ const interviewerMock = [
},
{ label: "james Doe", value: "2" },
{ label: "Jean Dupont", value: "3" },
+ { label: "enquêteur 1", value: "enquêteur 1" },
+ { label: "enquêteur 2", value: "enquêteur 2" },
+ { label: "enquêteur 3", value: "enquêteur 3" },
+ { label: "enquêteur 4", value: "enquêteur 4" },
+ { label: "enquêteur 5", value: "enquêteur 5" },
+ { label: "enquêteur 6", value: "enquêteur 6" },
+ { label: "enquêteur 7", value: "enquêteur 7" },
+ { label: "enquêteur 8", value: "enquêteur 8" },
+ { label: "enquêteur 9", value: "enquêteur 9" },
+ { label: "enquêteur 10", value: "enquêteur 10" },
+ { label: "enquêteur 11", value: "enquêteur 11" },
+ { label: "enquêteur 12", value: "enquêteur 12" },
+ { label: "enquêteur 13", value: "enquêteur 13" },
+];
+
+const surveysMock = [
+ { label: "enquête 1", value: "enquête 1" },
+ { label: "enquête 2", value: "enquête 2" },
+ { label: "enquête 3", value: "enquête 3" },
+ { label: "Autonomie", value: "Autonomie" },
+ { label: "Logement", value: "Logement" },
+];
+
+const subsampleMock = [
+ { label: "-", value: "undefined" },
+ { label: 10, value: "10" },
+ { label: 11, value: "11" },
];
export const FiltersCard = () => {
- const intl = useIntl();
+ const { translate } = useTranslation();
const filters = useGetSearchFilter();
const { onReset } = useSearchForm(filters);
const toggleSearchFilter = useToggleSearchFilter();
+ // TODO: find enum
+ const resultOptions = [].map(c => {
+ return { label: translate(c), value: c };
+ });
+
+ const statesOptions = surveyUnitStatesEnum.map(s => {
+ return { label: translate(s), value: s };
+ });
+
return (
- {intl.formatMessage({ id: "filterUnitsBy" })}
+ {translate("filterUnitsBy")}
{
canSearch={true}
/>
{
{getFiltersTags({
filters: filters.all,
- options: [...priorityOptions, ...closingCauseOptions, ...interviewerMock],
+ options: [
+ ...priorityOptions,
+ ...resultOptions,
+ ...statesOptions,
+ ...interviewerMock,
+ ...surveysMock,
+ ...subsampleMock,
+ ],
toggleSearchFilter,
})}
diff --git a/src/ui/Header.test.tsx b/src/ui/Header.test.tsx
index 4f3cccd..084f977 100644
--- a/src/ui/Header.test.tsx
+++ b/src/ui/Header.test.tsx
@@ -21,6 +21,14 @@ describe("Header component", () => {
expect(screen.getByText("Notifier")).toBeInTheDocument();
expect(screen.getByText("Réaffectation")).toBeInTheDocument();
expect(screen.getByText("Organisation des collectes")).toBeInTheDocument();
+ expect(screen.getByText("Mon profil")).toBeInTheDocument();
+ });
+
+ it("should open account menu and show name", () => {
+ WrappedRender();
+
+ fireEvent.click(screen.getByText("Mon profil"));
+
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
@@ -31,7 +39,7 @@ describe("Header component", () => {
});
WrappedRender();
- fireEvent.click(screen.getByText("John Doe"));
+ fireEvent.click(screen.getByText("Mon profil"));
const logoutButton = screen.getByText("Se déconnecter");
expect(logoutButton).toBeInTheDocument();
diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx
index a036769..29cc4a5 100644
--- a/src/ui/Header.tsx
+++ b/src/ui/Header.tsx
@@ -5,14 +5,14 @@ import { PropsWithChildren } from "react";
import packageInfo from "../../package.json";
import NotificationsIcon from "@mui/icons-material/Notifications";
import { AccountNavigation } from "./AccountNavigation.tsx";
-import { useIntl } from "react-intl";
+import { useTranslation } from "../hooks/useTranslation.ts";
const style = {
"&.MuiLink-root:hover": { color: "primary.main" },
};
export function Header() {
- const intl = useIntl();
+ const { translate } = useTranslation();
return (
@@ -41,16 +41,16 @@ export function Header() {
- {intl.formatMessage({ id: "goToFollowPage" })}
+ {translate("goToFollowPage")}
- {intl.formatMessage({ id: "goToReadPage" })}
+ {translate("goToReadPage")}
- {intl.formatMessage({ id: "goToClosePage" })}
+ {translate("goToClosePage")}
- {intl.formatMessage({ id: "goToNotifyPage" })}
+ {translate("goToNotifyPage")}
- {intl.formatMessage({ id: "goToCollectOrganization" })}
+ {translate("goToCollectOrganization")}
- {intl.formatMessage({ id: "goToReassignment" })}
+ {translate("goToReassignment")}
diff --git a/src/ui/HomeTable.test.tsx b/src/ui/HomeTable.test.tsx
new file mode 100644
index 0000000..033b64f
--- /dev/null
+++ b/src/ui/HomeTable.test.tsx
@@ -0,0 +1,46 @@
+import { fireEvent, screen } from "@testing-library/react";
+import { WrappedRender } from "../WrappedRender";
+import { HomeTable } from "./HomeTable";
+import { surveyUnitsMock } from "./HomeTableCard";
+import { UseMutationResult } from "@tanstack/react-query";
+import { APIError } from "../functions/api";
+import { APIEndpoints, APIMethods, APIRequest } from "../types/api";
+import * as hooks from "../hooks/useFetchQuery";
+
+describe("HomeTable component", () => {
+ const mockedResponse = {
+ mutateAsync: vi.fn(),
+ mutationFn: vi.fn(),
+ } as unknown as UseMutationResult<
+ never,
+ APIError,
+ Omit>, "method">,
+ unknown
+ >;
+
+ vi.spyOn(hooks, "useFetchMutation").mockReturnValue(mockedResponse);
+
+ it("should render HomeTable and change order when sort on id", () => {
+ WrappedRender();
+
+ expect(screen.getByText("Identifiant")).toBeInTheDocument();
+ expect(screen.getByText("Enquête")).toBeInTheDocument();
+ expect(screen.getByText("Sous-éch.")).toBeInTheDocument();
+ expect(screen.getByText("Enquêteur")).toBeInTheDocument();
+ expect(screen.getByText("Etat")).toBeInTheDocument();
+ expect(screen.getByText("Résultat")).toBeInTheDocument();
+ expect(screen.getByText("Bilan des contacts")).toBeInTheDocument();
+ expect(screen.getByText("Prioritaire")).toBeInTheDocument();
+ expect(screen.getByText("Actions")).toBeInTheDocument();
+ expect(screen.getAllByRole("row")[1]).toContain(screen.getByRole("cell", { name: /logement/i }));
+ expect(screen.getAllByRole("row")[2]).toContain(screen.getByRole("cell", { name: /autonomie/i }));
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: /identifiant/i,
+ }),
+ );
+ expect(screen.getAllByRole("row")[1]).toContain(screen.getByRole("cell", { name: /autonomie/i }));
+ expect(screen.getAllByRole("row")[2]).toContain(screen.getByRole("cell", { name: /logement/i }));
+ });
+});
diff --git a/src/ui/HomeTable.tsx b/src/ui/HomeTable.tsx
new file mode 100644
index 0000000..efe212c
--- /dev/null
+++ b/src/ui/HomeTable.tsx
@@ -0,0 +1,136 @@
+import { Table, TableBody, TableHead, TableRow } from "@mui/material";
+import TableContainer from "@mui/material/TableContainer";
+import { TableHeadCell } from "./TableHeadCell";
+import { useState } from "react";
+import { HomeTableRow } from "./HomeTableRow";
+import { TableFooter } from "./TableFooter";
+import { SurveyUnitTemporaryType } from "../types/temporaryTypes";
+
+type Props = {
+ surveyUnits: SurveyUnitTemporaryType[]; // TODO change type after backend rework
+};
+
+const columns = [
+ {
+ columnId: "id",
+ label: "id",
+ },
+ {
+ columnId: "campaignLabel",
+ label: "survey",
+ },
+ {
+ columnId: "ssech",
+ label: "subSample",
+ },
+ {
+ columnId: "interviewer",
+ label: "interviewer",
+ },
+ {
+ columnId: "states",
+ label: "state",
+ },
+ {
+ columnId: "result",
+ label: "result",
+ },
+ {
+ columnId: "contactOutcome",
+ label: "contactOutcome",
+ },
+ {
+ columnId: "priority",
+ label: "priority",
+ },
+ {
+ columnId: "actions",
+ label: "actions",
+ sort: false,
+ },
+];
+
+function descendingComparator(a: T, b: T, orderBy: keyof T) {
+ if (orderBy === "contactOutcome") {
+ const typeA = (a[orderBy] as SurveyUnitTemporaryType["contactOutcome"]).type;
+ const typeB = (b[orderBy] as SurveyUnitTemporaryType["contactOutcome"]).type;
+ return typeA.localeCompare(typeB);
+ }
+ if (b[orderBy] < a[orderBy]) {
+ return -1;
+ }
+ if (b[orderBy] > a[orderBy]) {
+ return 1;
+ }
+ return 0;
+}
+
+function getComparator(
+ order: "asc" | "desc",
+ orderBy: Key,
+): (
+ a: { [key in Key]: number | string | boolean | { date: number; type: string } },
+ b: { [key in Key]: number | string | boolean | { date: number; type: string } },
+) => number {
+ return order === "desc"
+ ? (a, b) => descendingComparator(a, b, orderBy)
+ : (a, b) => -descendingComparator(a, b, orderBy);
+}
+
+export const HomeTable = ({ surveyUnits }: Props) => {
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+ const [order, setOrder] = useState<"asc" | "desc">("asc");
+ const [orderBy, setOrderBy] = useState("id");
+
+ const handleChangePage = (_: React.MouseEvent | null, newPage: number) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (event: React.ChangeEvent) => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ };
+
+ const handleRequestSort = (_: React.MouseEvent, property: string) => {
+ const isAsc = orderBy === property && order === "asc";
+ setOrder(isAsc ? "desc" : "asc");
+ setOrderBy(property);
+ };
+
+ surveyUnits.sort(getComparator(order, orderBy));
+
+ return (
+
+
+
+
+ {columns.map(c => (
+
+ ))}
+
+
+
+ {surveyUnits.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(su => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/src/ui/HomeTableCard.test.tsx b/src/ui/HomeTableCard.test.tsx
new file mode 100644
index 0000000..f4b8b75
--- /dev/null
+++ b/src/ui/HomeTableCard.test.tsx
@@ -0,0 +1,56 @@
+import { screen, waitFor, waitForElementToBeRemoved } from "@testing-library/react";
+import { WrappedRender } from "../WrappedRender";
+import * as hooks from "../hooks/useFetchQuery";
+import { HomeTableCard } from "./HomeTableCard";
+import userEvent from "@testing-library/user-event";
+import { UseMutationResult } from "@tanstack/react-query";
+import { APIError } from "../functions/api";
+import { APIEndpoints, APIMethods, APIRequest } from "../types/api";
+
+describe("HomeTableCard component", () => {
+ const mockedResponse = {
+ mutateAsync: vi.fn(),
+ mutationFn: vi.fn(),
+ } as unknown as UseMutationResult<
+ never,
+ APIError,
+ Omit>, "method">,
+ unknown
+ >;
+
+ vi.spyOn(hooks, "useFetchMutation").mockReturnValue(mockedResponse);
+
+ it("should render HomeTable and filter by id", async () => {
+ WrappedRender();
+
+ expect(screen.getByRole("textbox", { name: /rechercher/i })).toBeInTheDocument();
+ expect(screen.getByText("Enquête")).toBeInTheDocument();
+ expect(screen.getByText("Sous-éch.")).toBeInTheDocument();
+ expect(screen.getByText("Enquêteur")).toBeInTheDocument();
+ expect(screen.getAllByRole("row")[1]).toContain(screen.getByRole("cell", { name: /logement/i }));
+ expect(screen.getAllByRole("row")[2]).toContain(screen.getByRole("cell", { name: /autonomie/i }));
+
+ await waitFor(() => userEvent.type(screen.getByRole("textbox"), "2"));
+ await waitForElementToBeRemoved(screen.queryByRole("cell", { name: /logement/i }));
+
+ expect(screen.getByRole("cell", { name: /autonomie/i })).toBeInTheDocument();
+ expect(screen.queryByRole("cell", { name: /logement/i })).not.toBeInTheDocument();
+ });
+
+ it("should render HomeTable and filter by campaign label", async () => {
+ WrappedRender();
+
+ expect(screen.getByRole("textbox", { name: /rechercher/i })).toBeInTheDocument();
+ expect(screen.getByText("Enquête")).toBeInTheDocument();
+ expect(screen.getByText("Sous-éch.")).toBeInTheDocument();
+ expect(screen.getByText("Enquêteur")).toBeInTheDocument();
+ expect(screen.getAllByRole("row")[1]).toContain(screen.getByRole("cell", { name: /logement/i }));
+ expect(screen.getAllByRole("row")[2]).toContain(screen.getByRole("cell", { name: /autonomie/i }));
+
+ await waitFor(() => userEvent.type(screen.getByRole("textbox"), "log"));
+ await waitForElementToBeRemoved(screen.queryByRole("cell", { name: /autonomie/i }));
+
+ expect(screen.queryByRole("cell", { name: /autonomie/i })).not.toBeInTheDocument();
+ expect(screen.getByRole("cell", { name: /logement/i })).toBeInTheDocument();
+ });
+});
diff --git a/src/ui/HomeTableCard.tsx b/src/ui/HomeTableCard.tsx
new file mode 100644
index 0000000..fd6bf3b
--- /dev/null
+++ b/src/ui/HomeTableCard.tsx
@@ -0,0 +1,97 @@
+import Card from "@mui/material/Card";
+import Stack from "@mui/material/Stack";
+import { SearchField } from "./SearchField";
+import { useDebouncedState } from "../hooks/useDebouncedState";
+import { HomeTable } from "./HomeTable";
+import { Filter, useGetSearchFilter } from "../hooks/useSearchFilter";
+import { SurveyUnitTemporaryType } from "../types/temporaryTypes";
+import { useTranslation } from "../hooks/useTranslation";
+
+export const surveyUnitsMock = [
+ {
+ id: "10000000000",
+ campaignLabel: "Logement",
+ ssech: 10,
+ interviewer: "enquêteur 1",
+ states: "CLO",
+ result: "NPA",
+ contactOutcome: {
+ date: 123,
+ type: "NOA",
+ },
+ priority: true,
+ },
+ {
+ id: "20000000000",
+ campaignLabel: "Autonomie",
+ ssech: 2,
+ interviewer: "enquêteur 2",
+ states: "FIN",
+ result: "NPI",
+ contactOutcome: {
+ date: 123,
+ type: "ALA",
+ },
+ priority: false,
+ },
+];
+
+export const HomeTableCard = () => {
+ const { translate } = useTranslation();
+ const [search, setSearch] = useDebouncedState("", 500);
+ const filters = useGetSearchFilter();
+
+ const filteredSurveyUnits = filterSurveyUnits({ surveyUnits: surveyUnitsMock ?? [], search, filters });
+
+ return (
+
+
+ setSearch(e.target.value)}
+ label={translate("toSearchLabel")}
+ placeholder={translate("searchSurveyUnitPlaceholder")}
+ />
+
+
+
+ );
+};
+
+type FilterSurveyUnitsProps = {
+ surveyUnits: SurveyUnitTemporaryType[]; // TODO change type after backend rework
+ search?: string;
+ filters: Filter;
+};
+
+const filterSurveyUnits = ({ surveyUnits, search, filters }: FilterSurveyUnitsProps) => {
+ if (search) {
+ surveyUnits = surveyUnits.filter(
+ item =>
+ item.id?.includes(search) ||
+ item.campaignLabel?.toLocaleLowerCase().includes(search.toLowerCase()),
+ );
+ }
+
+ filters.campaigns.length !== 0 &&
+ (surveyUnits = surveyUnits.filter(item => filters.campaigns.includes(item.campaignLabel)));
+
+ filters.ssech.length !== 0 &&
+ (surveyUnits = surveyUnits.filter(item => filters.ssech.toString().includes(item.ssech.toString())));
+
+ filters.interviewer.length !== 0 &&
+ (surveyUnits = surveyUnits.filter(item => filters.interviewer.includes(item.interviewer)));
+
+ filters.states.length !== 0 &&
+ (surveyUnits = surveyUnits.filter(item => filters.states.includes(item.states)));
+
+ filters.result.length !== 0 &&
+ (surveyUnits = surveyUnits.filter(item => filters.result.includes(item.result)));
+
+ filters.priority.length !== 0 &&
+ (surveyUnits = surveyUnits.filter(item =>
+ filters.priority.includes(item.priority ? "true" : "false"),
+ ));
+
+ return surveyUnits;
+};
diff --git a/src/ui/HomeTableRow.tsx b/src/ui/HomeTableRow.tsx
new file mode 100644
index 0000000..27f60da
--- /dev/null
+++ b/src/ui/HomeTableRow.tsx
@@ -0,0 +1,80 @@
+import TableRow from "@mui/material/TableRow";
+import TableCell from "@mui/material/TableCell";
+import { styled } from "@mui/material/styles";
+import { Row } from "./Row";
+import Tooltip from "@mui/material/Tooltip";
+import { Divider, IconButton } from "@mui/material";
+import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
+import InsertCommentIcon from "@mui/icons-material/InsertComment";
+import { SurveyUnitTemporaryType } from "../types/temporaryTypes";
+import { useToggle } from "react-use";
+import { CommentDialog } from "./CommentDialog";
+import { Link } from "./Link";
+import { StateChip } from "./StateChip";
+import ClearIcon from "@mui/icons-material/Clear";
+import { useTranslation } from "../hooks/useTranslation";
+
+type Props = {
+ surveyUnit: SurveyUnitTemporaryType; // TODO change type after backend rework
+};
+
+const StyledTableRow = styled(TableRow)(({ theme }) => ({
+ "&:nth-of-type(odd)": {
+ backgroundColor: theme.palette.action.hover,
+ },
+}));
+
+export const HomeTableRow = ({ surveyUnit }: Props) => {
+ const { translate } = useTranslation();
+
+ const [isDialogOpen, toggleDialog] = useToggle(false);
+
+ return (
+
+
+
+ {surveyUnit.id}
+
+
+ {surveyUnit.campaignLabel}
+ {surveyUnit.ssech ?? "-"}
+ {surveyUnit.interviewer ?? "-"}
+
+
+
+
+ {surveyUnit.result ? translate(surveyUnit.result) : "-"}
+
+
+ {surveyUnit.contactOutcome ? translate(surveyUnit.contactOutcome.type) : "-"}
+
+
+ {surveyUnit.priority ? translate("yes") : translate("no")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/ui/SearchField.tsx b/src/ui/SearchField.tsx
index dee8fc0..5abeb17 100644
--- a/src/ui/SearchField.tsx
+++ b/src/ui/SearchField.tsx
@@ -1,5 +1,5 @@
import InputAdornment from "@mui/material/InputAdornment";
-import TextField from "@mui/material/TextField";
+import TextField, { TextFieldProps } from "@mui/material/TextField";
import SearchIcon from "@mui/icons-material/Search";
import { ChangeEvent } from "react";
@@ -7,12 +7,12 @@ type Props = {
onChange: (e: ChangeEvent) => void;
label: string;
placeholder: string;
-};
+} & Pick;
-export const SearchField = ({ onChange, label, placeholder }: Props) => {
+export const SearchField = ({ onChange, label, placeholder, sx }: Props) => {
return (
{
ssech: [],
interviewer: [],
states: [],
- closingCause: [],
+ result: [],
priority: [],
all: [],
}}
diff --git a/src/ui/SelectWithCheckbox.tsx b/src/ui/SelectWithCheckbox.tsx
index 7de63af..e12ed15 100644
--- a/src/ui/SelectWithCheckbox.tsx
+++ b/src/ui/SelectWithCheckbox.tsx
@@ -1,12 +1,13 @@
import MenuItem from "@mui/material/MenuItem";
-import { Button, Checkbox, Popover, Typography } from "@mui/material";
+import { Box, Button, Checkbox, Popover, Typography } from "@mui/material";
import { useState } from "react";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import shadows from "@mui/material/styles/shadows";
import { Filter, emptyFilter } from "../hooks/useSearchFilter";
import { useDebouncedState } from "../hooks/useDebouncedState";
import { SearchField } from "./SearchField";
-import { useIntl } from "react-intl";
+import { StateChip } from "./StateChip";
+import { useTranslation } from "../hooks/useTranslation";
const style = {
root: {
@@ -16,7 +17,7 @@ const style = {
},
};
-export type Option = { label: string; value: string };
+export type Option = { label: string | number; value: string };
type Props = {
label: string;
@@ -35,7 +36,7 @@ export const SelectWithCheckbox = ({
toggleSearchFilter,
canSearch = false,
}: Props) => {
- const intl = useIntl();
+ const { translate } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", 500);
@@ -63,7 +64,7 @@ export const SelectWithCheckbox = ({
{label}
{canSearch && (
setDebouncedSearch(e.target.value)}
- label={intl.formatMessage({ id: "searchLabel" })}
- placeholder={intl.formatMessage({ id: "searchInterviewerPlaceholder" })}
+ label={translate("searchLabel")}
+ placeholder={translate("searchInterviewerPlaceholder")}
/>
)}
- {filteredOptions.map(option => (
-
- ))}
+
+ {filteredOptions.map(option => (
+
+ ))}
+
>
);
@@ -107,7 +115,7 @@ export const SelectWithCheckbox = ({
const filterOptions = ({ options, search }: { options: Option[]; search?: string }) => {
if (search) {
- return options.filter(item => item.label.toLowerCase().includes(search.toLowerCase()));
+ return options.filter(item => item.label.toString().toLowerCase().includes(search.toLowerCase()));
}
return options;
};
diff --git a/src/ui/StateChip.tsx b/src/ui/StateChip.tsx
new file mode 100644
index 0000000..d3b6045
--- /dev/null
+++ b/src/ui/StateChip.tsx
@@ -0,0 +1,25 @@
+import { Chip } from "@mui/material";
+import { useTranslation } from "../hooks/useTranslation";
+
+type Props = {
+ value: string;
+};
+
+export const StateChip = ({ value }: Props) => {
+ const { translate } = useTranslation();
+ return ;
+};
+
+const getChipColor = (value: string) => {
+ // TODO use real color and all states
+ switch (value) {
+ case "NVM":
+ return "success";
+ case "NNS":
+ return "error";
+ case "CLO":
+ return "info";
+ default:
+ return "default";
+ }
+};
diff --git a/src/ui/TableFooter.tsx b/src/ui/TableFooter.tsx
new file mode 100644
index 0000000..43256cc
--- /dev/null
+++ b/src/ui/TableFooter.tsx
@@ -0,0 +1,55 @@
+import { TableFooter as MuiTableFooter, TablePagination, TableRow } from "@mui/material";
+import { useTranslation } from "../hooks/useTranslation";
+
+const style = {
+ root: {
+ ".MuiTablePagination-displayedRows": {
+ typography: "bodySmall",
+ },
+ ".MuiTablePagination-input": {
+ typography: "bodySmall",
+ },
+ ".MuiTablePagination-selectLabel": {
+ typography: "bodySmall",
+ color: "text.tertiary",
+ },
+ borderBottom: "none",
+ },
+};
+
+type TableFooterProps = {
+ count: number;
+ rowsPerPage: number;
+ page: number;
+ onChangePage: (_: React.MouseEvent | null, newPage: number) => void;
+ onChangeRowsPerPage: (event: React.ChangeEvent) => void;
+};
+
+export const TableFooter = ({
+ count,
+ rowsPerPage,
+ page,
+ onChangePage,
+ onChangeRowsPerPage,
+}: TableFooterProps) => {
+ const { translate } = useTranslation();
+ return (
+
+
+
+ `${page.from}-${page.to === -1 ? page.count : page.to} ${translate("on")} ${page.count} ${translate("labelDisplayedRows")}`
+ }
+ count={count}
+ rowsPerPage={rowsPerPage}
+ page={page}
+ onPageChange={onChangePage}
+ onRowsPerPageChange={onChangeRowsPerPage}
+ />
+
+
+ );
+};
diff --git a/src/ui/TableHeadCell.test.tsx b/src/ui/TableHeadCell.test.tsx
new file mode 100644
index 0000000..9a83faa
--- /dev/null
+++ b/src/ui/TableHeadCell.test.tsx
@@ -0,0 +1,44 @@
+import { fireEvent, screen } from "@testing-library/react";
+import { WrappedRender } from "../WrappedRender";
+import { TableHeadCell } from "./TableHeadCell";
+
+describe("TableHeadCell component", () => {
+ const onRequestSortMock = vi.fn();
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should render label, sort icon and call sort function ", () => {
+ WrappedRender(
+ ,
+ );
+ expect(screen.getByText("Identifiant")).toBeInTheDocument();
+ expect(screen.getByTestId("ArrowDownwardIcon")).toBeInTheDocument();
+ fireEvent.click(screen.getByText("Identifiant"));
+ expect(onRequestSortMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("should only render label ", () => {
+ WrappedRender(
+ ,
+ );
+ expect(screen.getByText("Identifiant")).toBeInTheDocument();
+ expect(screen.queryByTestId("ArrowDownwardIcon")).not.toBeInTheDocument();
+ fireEvent.click(screen.getByText("Identifiant"));
+ expect(onRequestSortMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/ui/TableHeadCell.tsx b/src/ui/TableHeadCell.tsx
new file mode 100644
index 0000000..f819958
--- /dev/null
+++ b/src/ui/TableHeadCell.tsx
@@ -0,0 +1,44 @@
+import { TableCell as MuiTableCell, TableCellProps, TableSortLabel } from "@mui/material";
+import { useTranslation } from "../hooks/useTranslation";
+
+type Props = {
+ columnId: string;
+ label: string;
+ sort?: boolean;
+ order?: "asc" | "desc";
+ orderBy?: string;
+ onRequestSort?: (event: React.MouseEvent, property: string) => void;
+} & Pick;
+
+export const TableHeadCell = ({
+ columnId,
+ label,
+ sort = true,
+ order,
+ orderBy,
+ onRequestSort,
+ sx,
+}: Props) => {
+ const { translate } = useTranslation();
+
+ const createSortHandler = (property: string) => (event: React.MouseEvent) => {
+ onRequestSort?.(event, property);
+ };
+
+ return (
+
+ {sort ? (
+
+ {translate(label)}
+
+ ) : (
+ translate(label)
+ )}
+
+ );
+};