diff --git a/.vscode/settings.json b/.vscode/settings.json index c5cf6632f8d8c9dd3a503ba10ec94c4f01efe993..d2f1afdc05168415c392035b4aef1bc1351e3e85 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,7 +48,9 @@ "ecolyo", "enedis", "Enedis", + "firstname", "grdf", + "lastname", "luxon", "toastify", "wysiwyg", diff --git a/src/components/Consents/GrdfConsents.tsx b/src/components/Consents/GrdfConsents.tsx new file mode 100644 index 0000000000000000000000000000000000000000..af2f868500f09068859074feaae5ab44899e31e8 --- /dev/null +++ b/src/components/Consents/GrdfConsents.tsx @@ -0,0 +1,317 @@ +import { Button, TablePagination, TextField } from '@mui/material' +import { + ColDef, + ColGroupDef, + CsvExportParams, + GridApi, + GridReadyEvent, + IRowNode, + RowSelectedEvent, + ValueFormatterParams, +} from 'ag-grid-community' +import { AgGridReact } from 'ag-grid-react' +import { DateTime } from 'luxon' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useWhoAmI } from '../../API' +import { getAxiosXSRFHeader } from '../../axios.config' +import { IGrdfConsent } from '../../models/grdfConsent' +import { GrdfConsentService } from '../../services/grdfConsent.service' +import DownloadModal from './DownloadModal' +import './agGridOverrides.scss' +import styles from './consents.module.scss' + +export const GrdfConsents: React.FC = () => { + const [gridApi, setGridApi] = useState<GridApi | null>(null) + const [search, setSearch] = useState<string>('') + const [selectedNodes, setSelectedNodes] = useState<IRowNode[]>([]) + const [isShowingSelection, setIsShowingSelection] = useState<boolean>(false) + const [openDownloadModal, setOpenDownloadModal] = useState<boolean>(false) + const [consents, setConsents] = useState<IGrdfConsent[]>([]) + const [page, setPage] = useState<number>(0) + const [rowsPerPage, setRowsPerPage] = useState<number>(50) + const [totalRows, setTotalRows] = useState<number>(50) + const { data: user } = useWhoAmI() + const consentService = useMemo(() => { + return new GrdfConsentService() + }, []) + + const toggleOpenModal = useCallback(() => { + setOpenDownloadModal(prev => !prev) + }, []) + + const defaultColDef = useMemo( + () => ({ + sortable: true, + resizable: true, + }), + [] + ) + + const dateFormatter = (data: ValueFormatterParams): string => { + return (data.value as DateTime).toLocaleString() + } + + const [columnDefs] = useState<(ColDef | ColGroupDef)[] | null>([ + { + field: 'ID', + hide: true, + }, + { + field: 'pce', + headerName: 'N° PCE', + initialWidth: 180, + filter: true, + checkboxSelection: true, + }, + { + field: 'lastname', + headerName: 'Nom', + initialWidth: 180, + filter: true, + cellStyle: { textTransform: 'uppercase' }, + }, + { + field: 'firstname', + headerName: 'Prénom', + initialWidth: 180, + filter: true, + cellStyle: { textTransform: 'capitalize' }, + }, + { + field: 'postalCode', + headerName: 'CP', + initialWidth: 80, + filter: true, + }, + { + field: 'startDate', + valueFormatter: dateFormatter, + headerName: 'Début du consentement', + initialWidth: 150, + filter: true, + sort: 'desc', + }, + { + field: 'endDate', + valueFormatter: dateFormatter, + headerName: 'Fin du consentement', + initialWidth: 150, + filter: true, + }, + ]) + + const handleChangePage = useCallback( + ( + _event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, + newPage: number + ) => { + setPage(newPage) + }, + [] + ) + const handleChangeRowsPerPage = useCallback(event => { + setRowsPerPage(event.target.value) + setPage(0) + }, []) + + const checkSelectedNodes = useCallback(() => { + if (gridApi) { + const newNodes = gridApi.getRenderedNodes() + const idsToCheck: string[] = selectedNodes + .filter(node => node.isSelected) + .map(node => node.data.ID) + + newNodes.forEach(node => { + if (idsToCheck.includes(node.data.ID)) node.setSelected(true, false) + }) + } + }, [gridApi, selectedNodes]) + + const searchConsents = async () => { + if (user) { + const consentPagination = await consentService.searchConsents( + search, + rowsPerPage, + page, + getAxiosXSRFHeader(user.xsrftoken) + ) + if (consentPagination) { + setConsents(consentPagination.rows) + checkSelectedNodes() + setTotalRows(consentPagination.totalRows) + } + } + } + + const handleSearchChange = (newSearch: string) => { + setSearch(newSearch) + setPage(0) + } + + const resetSelection = useCallback(() => { + if (gridApi) { + setIsShowingSelection(false) + gridApi.setRowData(consents) + gridApi.deselectAll() + setSelectedNodes([]) + } + }, [gridApi, consents]) + + const onRowSelected = useCallback( + (event: RowSelectedEvent) => { + if (event.node.isSelected()) { + const index = selectedNodes.findIndex( + node => node.data.ID === event.node.data.ID + ) + if (index === -1) { + setSelectedNodes(prev => [...prev, event.node]) + } + } else { + setSelectedNodes(prev => + prev.filter(node => { + return node.data.ID != event.node.data.ID + }) + ) + } + }, + [selectedNodes] + ) + + const continueSelection = useCallback(() => { + if (gridApi) { + setIsShowingSelection(false) + gridApi?.setRowData(consents) + const newNodes = gridApi.getRenderedNodes() + // We have to select nodes that have already been selected since we cannot pass a Node array to init AgGrid + const idsToCheck: string[] = selectedNodes + .filter(node => node.isSelected) + .map(node => node.data.ID) + + newNodes.forEach(node => { + if (idsToCheck.includes(node.data.ID)) node.setSelected(true) + }) + } + }, [gridApi, consents, selectedNodes]) + + const showCurrentSelection = useCallback(() => { + setIsShowingSelection(true) + const dataFromNode = selectedNodes.map(item => item.data) + selectedNodes && gridApi?.setRowData(dataFromNode) + gridApi?.selectAll() + }, [gridApi, selectedNodes]) + + const exportData = useCallback(() => { + //You can change default column separator + const params: CsvExportParams = { + columnSeparator: ',', + } + gridApi?.exportDataAsCsv(params) + setOpenDownloadModal(false) + resetSelection() + }, [gridApi, resetSelection]) + + const onGridReady = ({ api }: GridReadyEvent) => { + //Grid init method + setGridApi(api) + api.sizeColumnsToFit() + } + + useEffect(() => { + function handleResize() { + gridApi?.sizeColumnsToFit() + } + handleResize() + window.addEventListener('resize', handleResize) + return () => { + window.removeEventListener('resize', handleResize) + } + }, [gridApi]) + + /** Trigger search when page loads or when admin changes input or pagination */ + useEffect(() => { + searchConsents() + // /!\ Do not change dependencies or effect will not trigger when pagination changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rowsPerPage, page, search]) + + return ( + <> + <div className="header"> + <h1>Consentements GRDF</h1> + </div> + <div className={styles.content}> + <TextField + placeholder="N°PCE (14 chiffres)" + label="Recherche" + value={search} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => + handleSearchChange(e.target.value) + } + disabled={isShowingSelection} + autoComplete="off" + /> + <div + className="ag-theme-alpine-dark" + style={{ width: '100%', height: '75vh' }} + > + <AgGridReact + onGridReady={onGridReady} + defaultColDef={defaultColDef} + rowHeight={35} + rowData={consents} + columnDefs={columnDefs} + animateRows={true} + rowSelection="multiple" + allowDragFromColumnsToolPanel={false} + onRowSelected={onRowSelected} + sortingOrder={['asc', 'desc']} + rowMultiSelectWithClick={true} + pagination={false} + suppressCellFocus={true} + rowClassRules={{ + expired: params => params.data.endDate < DateTime.now(), + }} + /> + {!isShowingSelection && ( + <TablePagination + labelRowsPerPage="Consentements par page" + component="div" + count={totalRows} + page={page} + onPageChange={handleChangePage} + rowsPerPage={rowsPerPage} + onRowsPerPageChange={handleChangeRowsPerPage} + rowsPerPageOptions={[10, 25, 50, 100]} + /> + )} + </div> + <DownloadModal + open={openDownloadModal} + toggleOpenModal={toggleOpenModal} + exportData={exportData} + /> + </div> + <div className={styles.footerButtons}> + <Button + variant="outlined" + onClick={isShowingSelection ? continueSelection : resetSelection} + disabled={ + !isShowingSelection && selectedNodes && selectedNodes.length === 0 + } + > + {isShowingSelection + ? 'Continuer ma sélection' + : 'Tout désélectionner'} + </Button> + <Button + onClick={!isShowingSelection ? showCurrentSelection : toggleOpenModal} + disabled={selectedNodes && selectedNodes.length <= 0} + classes={{ contained: styles.btnText }} + > + {!isShowingSelection ? 'Voir mes sélections' : 'Télécharger'} + <div>{selectedNodes?.length}</div> + </Button> + </div> + </> + ) +} diff --git a/src/components/Consents/Consents.tsx b/src/components/Consents/SgeConsents.tsx similarity index 98% rename from src/components/Consents/Consents.tsx rename to src/components/Consents/SgeConsents.tsx index 3ee1795cd2e64bf942fb02059a99936283619954..f31a476746e6e4c7b75bdba99a2052086d7d2bc6 100644 --- a/src/components/Consents/Consents.tsx +++ b/src/components/Consents/SgeConsents.tsx @@ -19,9 +19,8 @@ import { SgeConsentService } from '../../services/sgeConsent.service' import DownloadModal from './DownloadModal' import './agGridOverrides.scss' import styles from './consents.module.scss' -import './muiPaginationOverrides.scss' -const Consents: React.FC = () => { +export const SgeConsents: React.FC = () => { const [gridApi, setGridApi] = useState<GridApi | null>(null) const [search, setSearch] = useState<string>('') const [selectedNodes, setSelectedNodes] = useState<IRowNode[]>([]) @@ -254,7 +253,7 @@ const Consents: React.FC = () => { return ( <> <div className="header"> - <h1>Gestion des consentements Enedis</h1> + <h1>Consentements Enedis</h1> </div> <div className={styles.content}> <TextField @@ -332,4 +331,3 @@ const Consents: React.FC = () => { </> ) } -export default Consents diff --git a/src/components/Consents/muiPaginationOverrides.scss b/src/components/Consents/muiPaginationOverrides.scss deleted file mode 100644 index a759bd019b0e87beabe0086d6507b8ff39ca87c5..0000000000000000000000000000000000000000 --- a/src/components/Consents/muiPaginationOverrides.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Overrides MaterialUI Paginiation styles - -.MuiMenuItem-root { - color: black !important; -} diff --git a/src/components/Routes/Router.tsx b/src/components/Routes/Router.tsx index 386cf72d8c9b990353d12ce696c3a5877b6c4bdd..9b651a64d09b326796a7df3861a320e6d62efbf6 100644 --- a/src/components/Routes/Router.tsx +++ b/src/components/Routes/Router.tsx @@ -1,6 +1,7 @@ import { Navigate, Route, Routes } from 'react-router-dom' import { useWhoAmI } from '../../API' -import Consents from '../Consents/Consents' +import { GrdfConsents } from '../Consents/GrdfConsents' +import { SgeConsents } from '../Consents/SgeConsents' import Loader from '../Loader/Loader' import Login from '../Login/Login' import Newsletter from '../Newsletter/Newsletter' @@ -57,7 +58,16 @@ const Router = () => { <Route path={links.prices.path} element={<Prices />} /> <Route path="/popups" element={<Popups />} /> {user.isAdmin && ( - <Route path={links.sgeConsents.path} element={<Consents />} /> + <> + <Route + path={links.sgeConsents.path} + element={<SgeConsents />} + /> + <Route + path={links.grdfConsents.path} + element={<GrdfConsents />} + /> + </> )} <Route path="/login" element={<Login />} /> <Route diff --git a/src/models/grdfConsent.ts b/src/models/grdfConsent.ts index 20490a5944af648682f40adf45779b75544956b4..7f7e3c5f54952c713b566a14ecb2816034a3670b 100644 --- a/src/models/grdfConsent.ts +++ b/src/models/grdfConsent.ts @@ -1,7 +1,6 @@ import { DateTime } from 'luxon' - export interface IGrdfConsent - extends Omit<GrdfConsentEntity, 'CreatedAt' | 'endDate' | 'inseeCode'> { + extends Omit<GrdfConsentEntity, 'CreatedAt' | 'endDate'> { startDate: DateTime endDate: DateTime } @@ -12,12 +11,8 @@ export interface GrdfConsentEntity { endDate: string firstname: string lastname: string - pointID: number - address: string + pce: number postalCode: string - inseeCode: string - city: string - safetyOnBoarding: boolean } export interface IGrdfConsentPagination diff --git a/src/models/sgeConsent.model.ts b/src/models/sgeConsent.model.ts index fe09f1300080502b82424dd703dfef5283534ef0..224ba0e913409ea408c6669b234990d552960378 100644 --- a/src/models/sgeConsent.model.ts +++ b/src/models/sgeConsent.model.ts @@ -1,4 +1,5 @@ import { DateTime } from 'luxon' + export interface ISgeConsent extends Omit<SgeConsentEntity, 'CreatedAt' | 'endDate' | 'inseeCode'> { startDate: DateTime diff --git a/src/services/grdfConsent.service.ts b/src/services/grdfConsent.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6fe987d2871c6a13450fdcb8b3311ea6dcea294c --- /dev/null +++ b/src/services/grdfConsent.service.ts @@ -0,0 +1,83 @@ +import axios, { AxiosRequestConfig } from 'axios' +import { DateTime } from 'luxon' +import { toast } from 'react-toastify' +import { + GrdfConsentEntity, + GrdfConsentPaginationEntity, + IGrdfConsent, + IGrdfConsentPagination, +} from '../models/grdfConsent' + +export class GrdfConsentService { + /** + * Search for consents + * @param search + * @param limit + * @param page + * @param axiosHeaders + */ + public searchConsents = async ( + search: string, + limit: number, + page: number, + axiosHeaders: AxiosRequestConfig + ): Promise<IGrdfConsentPagination | null> => { + try { + const { data } = await axios.get<GrdfConsentPaginationEntity>( + `/api/admin/grdf/consent?search=${search}&limit=${limit}&page=${page}`, + axiosHeaders + ) + return this.parseConsentPagination(data) + } catch (e) { + if (e.response.status === 403) { + toast.error("Accès refusé : vous n'avez pas les droits nécessaires") + } else { + toast.error('Erreur lors de la récupération des consentements') + } + console.error(e) + return null + } + } + + /** + * Converts consent entity into consent + * @param consentEntity + */ + public parseConsent = (consentEntity: GrdfConsentEntity): IGrdfConsent => { + const startDate = DateTime.fromISO(consentEntity.CreatedAt, { + zone: 'utc', + }).setLocale('fr-FR') + const endDate = DateTime.fromISO(consentEntity.endDate, { + zone: 'utc', + }).setLocale('fr-FR') + + return { + ID: consentEntity.ID, + startDate: startDate, + endDate: endDate, + firstname: consentEntity.firstname, + lastname: consentEntity.lastname, + pce: consentEntity.pce, + postalCode: consentEntity.postalCode, + } + } + + /** + * Converts consent pagination entity into consent pagination + * @param consentPaginationEntity + */ + public parseConsentPagination = ( + consentPaginationEntity: GrdfConsentPaginationEntity + ): IGrdfConsentPagination => { + const rows = consentPaginationEntity.rows.map(consent => + this.parseConsent(consent) + ) + + const consentPagination: IGrdfConsentPagination = { + rows: rows, + totalRows: consentPaginationEntity.totalRows, + totalPages: consentPaginationEntity.totalPages, + } + return consentPagination + } +}