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")} { horizontal: "right", }} > - - {/* TODO: change link */} - - {intl.formatMessage({ id: "selectFavoriteSurveys" })} - + + {name} - - - - - {intl.formatMessage({ id: "goToHelp" })} - - - + + {/* TODO: change link */} + {translate("selectFavoriteSurveys").toLocaleUpperCase()} + + {/* TODO: change link */} + + + + {translate("goToHelp").toLocaleUpperCase()} + + + logout({ redirectTo: "specific url", @@ -77,7 +91,7 @@ export const AccountNavigation = () => { }) } > - {intl.formatMessage({ id: "logout" })} + {translate("logout")} 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 ( + +
+ + + {translate("comment")} + + + + {translate("surveyUnitNumber")} {surveyUnitId} + + + + + + + , + }} + id="comment" + name="comment" + label={translate("comment")} + placeholder={translate("commentPlaceholder")} + type="text" + fullWidth + variant="outlined" + multiline + rows={3} + value={newComment} + onChange={onChange} + /> + + {translate("commentDialogHelpText")} + + + + {comment === "" || isModified ? ( + + + + + ) : ( + + + + + )} +
+
+ ); +}; 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 => ( - { - toggleSearchFilter(name, option.value); - }} - sx={{ pl: 1, py: 0, minWidth: "150px" }} - > - ].includes(option.value)} - /> - - {option.label} - - - ))} + + {filteredOptions.map(option => ( + { + toggleSearchFilter(name, option.value); + }} + sx={{ pl: 1, py: 0, minWidth: "150px" }} + > + ].includes(option.value)} + /> + {name !== "states" ? ( + + {option.label} + + ) : ( + + )} + + ))} + ); @@ -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) + )} + + ); +};