From dd22698bdaacaba70942cf89f1fcaa8db4e76123 Mon Sep 17 00:00:00 2001 From: Mihoub Debache Date: Thu, 18 Jan 2024 15:11:47 +0100 Subject: [PATCH] fix(admin): update admin display, add filters, tags on modification journal and error message --- .../components/useModificationData.js | 12 +++ .../blocs/modification-journal/globalIndex.js | 93 +++++++++++++++++++ .../blocs/modification-journal/index.js | 91 ++---------------- src/components/errors/index.js | 11 ++- src/pages/admin/categories-juridiques.js | 62 +++++++++---- src/pages/admin/groupes.js | 3 +- src/pages/admin/journal.js | 4 +- src/pages/admin/nomenclatures.js | 63 ++++++++++--- src/pages/admin/relation-types.js | 20 ++-- src/pages/admin/users.js | 12 ++- src/pages/not-found/index.js | 54 ++++++++--- 11 files changed, 285 insertions(+), 140 deletions(-) create mode 100644 src/components/blocs/modification-journal/components/useModificationData.js create mode 100644 src/components/blocs/modification-journal/globalIndex.js diff --git a/src/components/blocs/modification-journal/components/useModificationData.js b/src/components/blocs/modification-journal/components/useModificationData.js new file mode 100644 index 00000000..6c9c47c6 --- /dev/null +++ b/src/components/blocs/modification-journal/components/useModificationData.js @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; +import useFetch from '../../../../hooks/useFetch'; + +const useModificationData = (days) => { + const queryDate = useMemo(() => new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(), [days]); + const { data, error } = useFetch(`/dashboard?filters[createdAt][$gte]=${queryDate}`); + const topUsers = data?.data?.[0]?.byUser.slice(0, 15) || []; + const userIDs = topUsers.map((item) => item._id); + return { topUsers, error, userIDs }; +}; + +export default useModificationData; diff --git a/src/components/blocs/modification-journal/globalIndex.js b/src/components/blocs/modification-journal/globalIndex.js new file mode 100644 index 00000000..593913b8 --- /dev/null +++ b/src/components/blocs/modification-journal/globalIndex.js @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { Accordion, AccordionItem, Col, Pagination, Row, Tag, TagGroup, Text } from '@dataesr/react-dsfr'; +import { useSearchParams } from 'react-router-dom'; +import useFetch from '../../../hooks/useFetch'; +import { Bloc, BlocContent, BlocTitle } from '../../bloc'; +import ModificationDetails from './components/modification-details'; +import ModificationTitle from './components/modification-title'; +import useModificationData from './components/useModificationData'; +import { PageSpinner } from '../../spinner'; + +const LAST_DAYS = 30; +const DATE = new Date(Date.now() - LAST_DAYS * 24 * 60 * 60 * 1000).toISOString(); +const ITEMS_PER_PAGE = 50; + +export default function GlobalModificationJournal() { + const { topUsers } = useModificationData(LAST_DAYS); + const [openAccordions, setOpenAccordions] = useState([]); + const [searchParams, setSearchParams] = useSearchParams(); + + const page = searchParams.get('page') || 1; + const id = searchParams.get('id'); + + const limit = ITEMS_PER_PAGE; + const skip = (page - 1) * limit; + + const url = id + ? `/journal?filters[userId]=${id}&filters[createdAt][$gte]=${DATE}&sort=-createdAt&limit=${limit}&skip=${skip}&filters[resourceType][$ne]=admin` + : `/journal?filters[createdAt][$gte]=${DATE}&sort=-createdAt&skip=${skip}&limit=${limit}&filters[resourceType][$ne]=admin`; + + const { data, error, isLoading } = useFetch(url); + + if (isLoading) return ; + if (!data?.data?.length) return `L'utilisateur n'a pas effectué de modification depuis les ${LAST_DAYS} derniers jours`; + + return ( + + + Journal des modifications + + + + Filtrer par utilisateurs ayant effectué une modification dans les 30 derniers jours : + + + {topUsers.map((user) => ( + { + if (id === user._id) { + setSearchParams({}); + } else { setSearchParams({ id: user._id }); } + }} + selected={id === user._id} + className="no-span" + > + {user.displayName} + {' '} + ( + {user.totalOperations} + ) + + ))} + + + + + {data?.data.map((event, i) => ( + setOpenAccordions((prev) => [...prev, i])} + title={} + > + {openAccordions.includes(i) && } + + ))} + + { + searchParams.set('page', newPage); + setSearchParams(searchParams); + }} + surroundingPages={2} + currentPage={Number(page)} + pageCount={data?.totalCount ? Math.ceil(data.totalCount / ITEMS_PER_PAGE) : 0} + /> + + + + + + + ); +} diff --git a/src/components/blocs/modification-journal/index.js b/src/components/blocs/modification-journal/index.js index 201fcd34..2adc4534 100644 --- a/src/components/blocs/modification-journal/index.js +++ b/src/components/blocs/modification-journal/index.js @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import { Accordion, AccordionItem, Col, Pagination, Row, Tag, TagGroup } from '@dataesr/react-dsfr'; -import { useParams, useSearchParams } from 'react-router-dom'; +import { Accordion, AccordionItem, Col, Row } from '@dataesr/react-dsfr'; +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; import useFetch from '../../../hooks/useFetch'; import { Bloc, BlocContent, BlocTitle } from '../../bloc'; import ModificationDetails from './components/modification-details'; @@ -11,99 +11,26 @@ const DATE = new Date(Date.now() - LAST_DAYS * 24 * 60 * 60 * 1000).toISOString( export default function ModificationJournal() { const [openAccordions, setOpenAccordions] = useState([]); - const [searchParams, setSearchParams] = useSearchParams(); - const [filterName, setFilterName] = useState(''); - const [selectedUser, setSelectedUser] = useState(''); - - const page = searchParams.get('page') || 1; - const limit = 50; - const skip = (page - 1) * 50; const { id: resourceId } = useParams(); const url = resourceId - ? `/journal?filters[resourceId]=${resourceId}&filters[createdAt][$gte]=${DATE}&sort=-createdAt&skip=${skip}&limit=${limit}&filters[resourceType][$ne]=admin&filters[resourceType][$ne]=groups` - : `/journal?filters[createdAt][$gte]=${DATE}&sort=-createdAt&skip=${skip}&limit=${limit}&filters[resourceType][$ne]=admin`; - + ? `/journal?filters[resourceId]=${resourceId}&filters[createdAt][$gte]=${DATE}&sort=-createdAt&limit=100&filters[resourceType][$ne]=admin&filters[resourceType][$ne]=groups` + : `/journal?filters[createdAt][$gte]=${DATE}&sort=-createdAt&limit=100&filters[resourceType][$ne]=admin`; const { data, error, isLoading } = useFetch(url); - - if (!data?.totalCount) { - return null; - } - const uniqueUserNames = Array.from( - new Set(data?.data?.map((el) => `${el.user.firstName} ${el.user.lastName}`) || []), - ); - - const handleUserFilter = (userName) => { - setSearchParams({ page: 1 }); - - if (selectedUser === userName) { - setSelectedUser(''); - setFilterName(''); - } else { - setSelectedUser(userName); - setFilterName(userName); - } - }; - - const sortedUniqueUserNames = [...uniqueUserNames].sort((a, b) => { - const countA = data.data.filter( - (event) => `${event.user.firstName} ${event.user.lastName}` === a, - ).length; - const countB = data.data.filter( - (event) => `${event.user.firstName} ${event.user.lastName}` === b, - ).length; - return countB - countA; - }); - return ( - Journal des modifications + Journal des modifications - - {sortedUniqueUserNames.map((userName) => { - const modificationsCount = data?.data?.filter( - (el) => `${el.user.firstName} ${el.user.lastName}` === userName, - ).length; - return ( - handleUserFilter(userName)} - selected={selectedUser === userName} - className="no-span" - > - {userName} - {' '} - ( - {modificationsCount} - ) - - ); - })} - - {data?.data?.length > 0 - && data.data.map((event, i) => ( - (!filterName || filterName === `${event.user.firstName} ${event.user.lastName}`) && ( - setOpenAccordions((prev) => [...prev, i])} - key={event.createdAt} - title={} - > + {data?.data?.length && data.data.map((event, i) => ( + setOpenAccordions((prev) => [...prev, i])} key={event.createdAt} title={}> {openAccordions.includes(i) && } - ) - ))} + ))} - - setSearchParams({ page: newPage })} - surrendingPages={2} - currentPage={Number(page)} - pageCount={data?.totalCount ? Math.ceil(data.totalCount / limit) : 0} - /> - ); diff --git a/src/components/errors/index.js b/src/components/errors/index.js index 182b739d..26fbc73c 100644 --- a/src/components/errors/index.js +++ b/src/components/errors/index.js @@ -2,6 +2,7 @@ import { ButtonGroup, Col, Container, Row, Text, Title } from '@dataesr/react-ds import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { ReactComponent as TechErrorSVG } from '../../assets/technical-error.svg'; +import useAuth from '../../hooks/useAuth'; function Error500() { return ( @@ -20,7 +21,7 @@ function Error404() { <> Page non trouvée Erreur 404 - La page que vous cherchez est introuvable. Excusez-nous pour la gêne occasionnée. + La page que vous cherchez est introuvable. Ou vous n'êtes peut être pas connecté avec vos identifiants. Excusez-nous pour la gêne occasionnée. Si vous avez tapé l'adresse web dans le navigateur, vérifiez qu'elle est correcte. La page n'est peut-être plus disponible.
@@ -33,6 +34,7 @@ function Error404() { } export default function Error({ status }) { + const { viewer } = useAuth(); return ( @@ -49,6 +51,13 @@ export default function Error({ status }) { Contactez-nous + {!viewer && ( +
  • + + Connectez-vous + +
  • + ) } diff --git a/src/pages/admin/categories-juridiques.js b/src/pages/admin/categories-juridiques.js index e8a61f65..44f362df 100644 --- a/src/pages/admin/categories-juridiques.js +++ b/src/pages/admin/categories-juridiques.js @@ -1,21 +1,28 @@ -import { Badge, Breadcrumb, BreadcrumbItem, Col, Container, Modal, ModalContent, ModalTitle, Row, Tag, Text, Title } from '@dataesr/react-dsfr'; +import { Badge, Breadcrumb, BreadcrumbItem, Col, Container, Link, Modal, ModalContent, ModalTitle, Row, Tag, Text, TextInput, Title } from '@dataesr/react-dsfr'; import { useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import Button from '../../components/button'; +import CopyButton from '../../components/copy/copy-button'; import LegalCategoriesForm from '../../components/forms/legal-categories'; import useFetch from '../../hooks/useFetch'; import useNotice from '../../hooks/useNotice'; import api from '../../utils/api'; import { toString } from '../../utils/dates'; import { deleteError, deleteSuccess, saveError, saveSuccess } from '../../utils/notice-contents'; +import { PageSpinner } from '../../components/spinner'; export default function LegalCategoriesPage() { const route = '/legal-categories'; const { data, isLoading, error, reload } = useFetch(`${route}?limit=500`); - const [isOpen, setIsOpen] = useState(); + const [isOpen, setIsOpen] = useState(false); const [modalTitle, setModalTitle] = useState(''); const [modalContent, setModalContent] = useState(null); const { notice } = useNotice(); + const [query, setQuery] = useState(''); + + const filteredData = query + ? (data?.data || []).filter((item) => item?.longNameFr?.toLowerCase().includes(query?.toLowerCase())) + : data?.data || []; const handleSave = async (body, itemId) => { const method = itemId ? 'patch' : 'post'; @@ -54,7 +61,8 @@ export default function LegalCategoriesPage() { }; if (error) return
    Erreur
    ; - if (isLoading) return
    Chargement
    ; + if (isLoading) return ; + return ( @@ -66,23 +74,45 @@ export default function LegalCategoriesPage() { - - - Catégories juridiques - - + + + Catégories juridiques + <Badge type="info" text={filteredData?.length} /> + -
    - {data.data?.map((item) => ( -
    - -
    - {item.longNameFr} + + + + Filtrer par catégories juridiques : + + setQuery(e.target.value)} size="sm" /> + + + {filteredData.map((item) => ( +
    + + + + {item.longNameFr} + Autres noms : - {item.otherNames.length ? item.otherNames.map((name) => {name}) : Aucun alias pour le moment} + {item.otherNames.length ? item.otherNames.map((name) => {name}) : Aucun alias pour le moment} + {item?.id && ( + + + ID : + {' '} + {item.id} + + + + )} Créé le {' '} @@ -99,7 +129,7 @@ export default function LegalCategoriesPage() { {`${item.updatedBy?.firstName} ${item.updatedBy?.lastName}`} )} -
    +
    diff --git a/src/pages/admin/groupes.js b/src/pages/admin/groupes.js index 5089ea25..ea457c9d 100644 --- a/src/pages/admin/groupes.js +++ b/src/pages/admin/groupes.js @@ -9,6 +9,7 @@ import { Bloc, BlocActionButton, BlocContent, BlocModal, BlocTitle } from '../.. import useNotice from '../../hooks/useNotice'; import { saveError, saveSuccess, deleteSuccess, deleteError } from '../../utils/notice-contents'; import useEditMode from '../../hooks/useEditMode'; +import { PageSpinner } from '../../components/spinner'; export default function GroupsPage() { const url = '/groups'; @@ -72,7 +73,7 @@ export default function GroupsPage() { )); }; if (error) return
    Erreur
    ; - if (isLoading) return
    Chargement
    ; + if (isLoading) return ; return ( <> diff --git a/src/pages/admin/journal.js b/src/pages/admin/journal.js index de355391..f1adc598 100644 --- a/src/pages/admin/journal.js +++ b/src/pages/admin/journal.js @@ -1,6 +1,6 @@ import { Breadcrumb, BreadcrumbItem, Col, Container, Row } from '@dataesr/react-dsfr'; import { Link as RouterLink } from 'react-router-dom'; -import ModificationJournal from '../../components/blocs/modification-journal'; +import GlobalModificationJournal from '../../components/blocs/modification-journal/globalIndex'; export default function AdminJournalPage() { return ( @@ -14,7 +14,7 @@ export default function AdminJournalPage() { - + ); } diff --git a/src/pages/admin/nomenclatures.js b/src/pages/admin/nomenclatures.js index 788e5535..3404daf9 100644 --- a/src/pages/admin/nomenclatures.js +++ b/src/pages/admin/nomenclatures.js @@ -1,6 +1,6 @@ -import { Badge, Breadcrumb, BreadcrumbItem, Col, Container, Modal, ModalContent, ModalTitle, Row, Tag, Text, Title } from '@dataesr/react-dsfr'; -import PropTypes from 'prop-types'; import { useState } from 'react'; +import { Badge, Breadcrumb, BreadcrumbItem, Col, Container, Modal, ModalContent, ModalTitle, Row, Tag, Text, TextInput, Title } from '@dataesr/react-dsfr'; +import PropTypes from 'prop-types'; import { Link as RouterLink } from 'react-router-dom'; import Button from '../../components/button'; import NomenclatureForm from '../../components/forms/nomenclatures'; @@ -9,13 +9,19 @@ import useNotice from '../../hooks/useNotice'; import api from '../../utils/api'; import { toString } from '../../utils/dates'; import { deleteError, deleteSuccess, saveError, saveSuccess } from '../../utils/notice-contents'; +import CopyButton from '../../components/copy/copy-button'; +import { capitalize } from '../../utils/strings'; +import { PageSpinner } from '../../components/spinner'; export default function NomenclaturesPage({ route, title }) { const { data, isLoading, error, reload } = useFetch(`${route}?limit=500`); const { notice } = useNotice(); - const [isOpen, setIsOpen] = useState(); + const [isOpen, setIsOpen] = useState(false); const [modalTitle, setModalTitle] = useState(''); const [modalContent, setModalContent] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + + const filteredData = data?.data.filter((item) => item.usualName.toLowerCase().includes(searchQuery.toLowerCase())); const handleSave = async (body, itemId) => { const method = itemId ? 'patch' : 'post'; @@ -54,7 +60,8 @@ export default function NomenclaturesPage({ route, title }) { }; if (error) return
    Erreur
    ; - if (isLoading) return
    Chargement
    ; + if (isLoading) return ; + return ( @@ -67,22 +74,52 @@ export default function NomenclaturesPage({ route, title }) { - - {title} + + {title} <Badge type="info" text={data?.totalCount} /> - </Row> + -
    - {data.data?.map((item) => ( + + + + + Filtrer par + {' '} + {title} + : + + + setSearchQuery(e.target.value)} + /> + + + {filteredData?.map((item) => ( <> -
    - {item.usualName} + + {capitalize(item.usualName)} Autres noms : - {item.otherNames.length ? item.otherNames.map((name) => {name}) : Aucun alias pour le moment} + {item.otherNames.length ? item.otherNames.map((name) => {capitalize(name)}) : Aucun alias pour le moment} + {item?.id && ( + + + ID : + {' '} + {item.id} + + + + )} Créé le {' '} @@ -99,7 +136,7 @@ export default function NomenclaturesPage({ route, title }) { {`${item.updatedBy?.firstName} ${item.updatedBy?.lastName}`} )} -
    +
    diff --git a/src/pages/admin/relation-types.js b/src/pages/admin/relation-types.js index b83f9a59..31681c97 100644 --- a/src/pages/admin/relation-types.js +++ b/src/pages/admin/relation-types.js @@ -11,6 +11,7 @@ import { toString } from '../../utils/dates'; import { deleteError, deleteSuccess, saveError, saveSuccess } from '../../utils/notice-contents'; import { normalize } from '../../utils/strings'; import CopyButton from '../../components/copy/copy-button'; +import { PageSpinner } from '../../components/spinner'; function getSearchableRelationType(relationType) { const { name, maleName, feminineName, mandateTypeGroup, otherNames = [], for: relationFor = [] } = relationType; @@ -64,7 +65,7 @@ export default function RelationTypesPage() { }; if (error) return
    Erreur
    ; - if (isLoading) return
    Chargement
    ; + if (isLoading) return ; const filteredData = query ? data?.data?.filter((item) => getSearchableRelationType(item).includes(normalize(debouncedQuery))) : data?.data; @@ -86,14 +87,17 @@ export default function RelationTypesPage() {
    -
    - - setQuery(e.target.value)} size="sm" /> + + + + Filtrer par types de relations : + + setQuery(e.target.value)} size="sm" /> + -
    {filteredData?.map((item) => ( - - + <> + {item.name} @@ -172,7 +176,7 @@ export default function RelationTypesPage() {
    -
    + ))} setIsOpen(false)}> diff --git a/src/pages/admin/users.js b/src/pages/admin/users.js index c981cc4d..b1664fa9 100644 --- a/src/pages/admin/users.js +++ b/src/pages/admin/users.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Badge, ButtonGroup, + Button, Breadcrumb, BreadcrumbItem, Col, @@ -17,17 +18,18 @@ import { TagGroup, Tag, TextInput, + Link, } from '@dataesr/react-dsfr'; import { Link as RouterLink } from 'react-router-dom'; import Avatar from '../../components/avatar'; import useFetch from '../../hooks/useFetch'; import useToast from '../../hooks/useToast'; import api from '../../utils/api'; -import Button from '../../components/button'; import PaysageBlame from '../../components/paysage-blame'; import { normalize } from '../../utils/strings'; import useDebounce from '../../hooks/useDebounce'; +import { PageSpinner } from '../../components/spinner'; const getSearchableUser = (user) => { const { firstName, lastName, email, role, groups } = user; @@ -120,6 +122,11 @@ function User({ )} )} + + + Voir les dernières modifications de cet utilisateur + + Groupes : @@ -128,6 +135,7 @@ function User({ Aucun groupe n'a été défini pour cet utilisateur. )} + {(groups?.length > 0) && ( {groups.map((group) => ({group.acronym || group.name}))} @@ -398,7 +406,7 @@ export default function AdminUsersPage() {
    {(error) &&
    Erreur
    } - {(isLoading) &&
    Chargement
    } + {(isLoading) && } {(filteredUsers?.length > 0) && filteredUsers.map((item) => (
    Page Introuvable! - Désolé, la page que vous cherchez n'existe pas. + Désolé, la page que vous cherchez n'existe pas, ou bien vous n'êtes pas connecté avec vos identifiants. Vous pouvez relancer une recherche ou vous rendre sur la page d'accueil
    - + + + + + + + + + +
    -
    - ) : (
    @@ -56,15 +67,28 @@ export default function NotFound() {
    - Désolé, la page que vous cherchez n'existe pas. + Désolé, la page que vous cherchez n'existe pas, ou bien vous n'êtes pas connecté avec vos identifiants. Vous pouvez relancer une recherche ou vous rendre sur la page d'accueil - + + + + + + + + + +