From 723cb1e7a48ceb5e2541ba997b1af75f3d013c6b Mon Sep 17 00:00:00 2001 From: Arnaud AMBROSELLI Date: Fri, 8 Sep 2023 11:56:28 +0200 Subject: [PATCH] feat: historique pour les actions --- app/src/recoil/actions.js | 13 ++ app/src/scenes/Actions/Action.js | 25 +-- dashboard/src/components/ActionModal.js | 136 +++++++------ dashboard/src/components/PersonName.js | 13 +- dashboard/src/recoil/actions.js | 13 +- .../scenes/person/components/PersonHistory.js | 181 +++++++++--------- 6 files changed, 212 insertions(+), 169 deletions(-) diff --git a/app/src/recoil/actions.js b/app/src/recoil/actions.js index d9381d19d..c8bd50f14 100644 --- a/app/src/recoil/actions.js +++ b/app/src/recoil/actions.js @@ -44,6 +44,19 @@ const encryptedFields = [ 'history', ]; +export const allowedActionFieldsInHistory = [ + { name: 'categories', label: 'Catégorie(s)' }, + { name: 'person', label: 'Personne suivie' }, + { name: 'group', label: 'Action familiale' }, + { name: 'name', label: "Nom de l'action" }, + { name: 'description', label: 'Description' }, + { name: 'teams', label: 'Équipe(s) en charge' }, + { name: 'urgent', label: 'Action urgente' }, + { name: 'completedAt', label: 'Faite le' }, + { name: 'dueAt', label: 'À faire le' }, + { name: 'status', label: 'Status' }, +]; + export const prepareActionForEncryption = (action) => { try { if (!looseUuidRegex.test(action.person)) { diff --git a/app/src/scenes/Actions/Action.js b/app/src/scenes/Actions/Action.js index 9df21c7f1..551c197d9 100644 --- a/app/src/scenes/Actions/Action.js +++ b/app/src/scenes/Actions/Action.js @@ -21,7 +21,7 @@ import ActionCategoriesModalSelect from '../../components/ActionCategoriesModalS import Label from '../../components/Label'; import Tags from '../../components/Tags'; import { MyText } from '../../components/MyText'; -import { actionsState, DONE, CANCEL, TODO, prepareActionForEncryption, mappedIdsToLabels } from '../../recoil/actions'; +import { actionsState, DONE, CANCEL, TODO, prepareActionForEncryption, mappedIdsToLabels, allowedActionFieldsInHistory } from '../../recoil/actions'; import { useRecoilState, useRecoilValue } from 'recoil'; import { commentsState, prepareCommentForEncryption } from '../../recoil/comments'; import API from '../../services/api'; @@ -205,6 +205,18 @@ const Action = ({ navigation, route }) => { } } delete action.team; + + const historyEntry = { + date: new Date(), + user: user._id, + data: {}, + }; + for (const key in action) { + if (!allowedActionFieldsInHistory.map((field) => field.name).includes(key)) continue; + if (action[key] !== oldAction[key]) historyEntry.data[key] = { oldValue: oldAction[key], newValue: action[key] }; + } + if (!!Object.keys(historyEntry.data).length) action.history = [...(action.history || []), historyEntry]; + const response = await API.put({ path: `/action/${oldAction._id}`, body: prepareActionForEncryption(action), @@ -218,17 +230,6 @@ const Action = ({ navigation, route }) => { }) ); if (!!newAction.completedAt) await createReportAtDateIfNotExist(newAction.completedAt); - if (!statusChanged) return response; - const comment = { - comment: `${user.name} a changé le status de l'action: ${mappedIdsToLabels.find((status) => status._id === newAction.status)?.name}`, - action: actionDB?._id, - team: currentTeam._id, - user: user._id, - organisation: organisation._id, - date: new Date().toISOString(), - }; - const commentResponse = await API.post({ path: '/comment', body: prepareCommentForEncryption(comment) }); - if (commentResponse.ok) setComments((comments) => [commentResponse.decryptedData, ...comments]); return response; } catch (error) { capture(error, { extra: { message: 'error in updating action', action } }); diff --git a/dashboard/src/components/ActionModal.js b/dashboard/src/components/ActionModal.js index c0831d0a2..d3b8fc876 100644 --- a/dashboard/src/components/ActionModal.js +++ b/dashboard/src/components/ActionModal.js @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState, useRef } from 'react'; import { toast } from 'react-toastify'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useHistory, useLocation } from 'react-router-dom'; -import { actionsState, allowedActionFieldsInHistory, CANCEL, DONE, mappedIdsToLabels, prepareActionForEncryption, TODO } from '../recoil/actions'; +import { actionsState, allowedActionFieldsInHistory, CANCEL, DONE, prepareActionForEncryption, TODO } from '../recoil/actions'; import { currentTeamState, organisationState, teamsState, userState } from '../recoil/auth'; import { dayjsInstance, now, outOfBoundariesDate } from '../services/date'; import API from '../services/api'; @@ -289,7 +289,7 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM data: {}, }; for (const key in body) { - if (!allowedActionFieldsInHistory.includes(key)) continue; + if (!allowedActionFieldsInHistory.map((field) => field.name).includes(key)) continue; if (body[key] !== action[key]) historyEntry.data[key] = { oldValue: action[key], newValue: body[key] }; } if (!!Object.keys(historyEntry.data).length) body.history = [...(action.history || []), historyEntry]; @@ -312,17 +312,6 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM await createReportAtDateIfNotExist(newAction.completedAt); } } - if (statusChanged) { - const comment = { - comment: `${user.name} a changé le status de l'action: ${mappedIdsToLabels.find((status) => status._id === newAction.status)?.name}`, - action: action._id, - team: currentTeam._id, - user: user._id, - organisation: organisation._id, - }; - const commentResponse = await API.post({ path: '/comment', body: prepareCommentForEncryption(comment) }); - if (commentResponse.ok) setComments((comments) => [commentResponse.decryptedData, ...comments]); - } toast.success('Mise à jour !'); if (location.pathname !== '/stats') refresh(); // if we refresh when we're on stats page, it will remove the view we're on const actionCancelled = action.status !== CANCEL && body.status === CANCEL; @@ -658,6 +647,12 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM /> )} +
+ +
@@ -692,58 +687,81 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM } function ActionHistory({ action }) { - const history = useMemo(() => (action.history || []).reverse(), [action.history]); + const history = useMemo(() => [...(action?.history || [])].reverse(), [action?.history]); + const teams = useRecoilValue(teamsState); return (
-
-

Historique

-
- - - - - - - - - - {history.map((h) => { - return ( - - - - + + ); + })} + +
DateUtilisateurDonnée
{dayjsInstance(h.date).format('DD/MM/YYYY HH:mm')} - - - {Object.entries(h.data).map(([key, value]) => { - const personField = personFieldsIncludingCustomFields.find((f) => f.name === key); - if (key === 'teams') { + {!history?.length ? ( +
+

Cette action n'a pas encore d'historique.

+

+ Lorsqu'une action est modifiée, les changements sont enregistrés dans un historique, +
+ que vous pourrez ainsi retrouver sur cette page. +

+
+ ) : ( + + + + + + + + + + {history.map((h) => { + return ( + + + + - - ); - })} - -
DateUtilisateurDonnée
{dayjsInstance(h.date).format('DD/MM/YYYY HH:mm')} + + + {Object.entries(h.data).map(([key, value]) => { + const actionField = allowedActionFieldsInHistory.find((f) => f.name === key); + if (key === 'teams') { + return ( +

+ {actionField?.label} : + "{(value.oldValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" + + "{(value.newValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" +

+ ); + } + if (key === 'person') { + return ( +

+ {actionField?.label} :
+ + + {' '} + ➔{' '} + + + +

+ ); + } + return ( -

- {personField?.label} : - "{(value.oldValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" - - "{(value.newValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" +

+ {actionField?.label} :
+ {JSON.stringify(value.oldValue || '')}{JSON.stringify(value.newValue)}

); - } - - return ( -

- {personField?.label} :
- {JSON.stringify(value.oldValue || '')}{JSON.stringify(value.newValue)} -

- ); - })} -
+ })} +
+ )}
); } diff --git a/dashboard/src/components/PersonName.js b/dashboard/src/components/PersonName.js index c115ed736..7791b8207 100644 --- a/dashboard/src/components/PersonName.js +++ b/dashboard/src/components/PersonName.js @@ -1,5 +1,4 @@ import React from 'react'; -import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import { personsObjectSelector } from '../recoil/selectors'; @@ -9,20 +8,14 @@ export default function PersonName({ item, onClick = null, redirectToTab = 'Rés const persons = useRecoilValue(personsObjectSelector); const personName = item?.personPopulated?.name || persons[item.person]?.name; return ( - { e.stopPropagation(); if (onClick) return onClick(); if (item.person) history.push(`/person/${item.person}?tab=${redirectToTab}`); }}> {personName} - + ); } - -const BoldOnHover = styled.span` - &:hover { - background-color: yellow; - cursor: zoom-in; - } -`; diff --git a/dashboard/src/recoil/actions.js b/dashboard/src/recoil/actions.js index 802cb385e..5be0c19ac 100644 --- a/dashboard/src/recoil/actions.js +++ b/dashboard/src/recoil/actions.js @@ -45,7 +45,18 @@ const encryptedFields = [ 'history', ]; -export const allowedActionFieldsInHistory = encryptedFields.filter((field) => !['category', 'team', 'history'].includes(field)); +export const allowedActionFieldsInHistory = [ + { name: 'categories', label: 'Catégorie(s)' }, + { name: 'person', label: 'Personne suivie' }, + { name: 'group', label: 'Action familiale' }, + { name: 'name', label: "Nom de l'action" }, + { name: 'description', label: 'Description' }, + { name: 'teams', label: 'Équipe(s) en charge' }, + { name: 'urgent', label: 'Action urgente' }, + { name: 'completedAt', label: 'Faite le' }, + { name: 'dueAt', label: 'À faire le' }, + { name: 'status', label: 'Status' }, +]; export const prepareActionForEncryption = (action, { checkRequiredFields = true } = {}) => { if (!!checkRequiredFields) { diff --git a/dashboard/src/scenes/person/components/PersonHistory.js b/dashboard/src/scenes/person/components/PersonHistory.js index 69211e5f3..f16b73c24 100644 --- a/dashboard/src/scenes/person/components/PersonHistory.js +++ b/dashboard/src/scenes/person/components/PersonHistory.js @@ -1,7 +1,5 @@ import { useMemo } from 'react'; -import { Col, Row } from 'reactstrap'; import { useRecoilValue } from 'recoil'; -import { Title } from '../../../components/header'; import UserName from '../../../components/UserName'; import { teamsState } from '../../../recoil/auth'; import { personFieldsIncludingCustomFieldsSelector } from '../../../recoil/persons'; @@ -23,94 +21,103 @@ export default function PersonHistory({ person }) { return (
- - - Historique - - - - - - - - - - - - {history.map((h) => { - return ( - - - - + + ); + })} + +
DateUtilisateurDonnée
{dayjsInstance(h.date).format('DD/MM/YYYY HH:mm')} - - - {Object.entries(h.data).map(([key, value]) => { - const personField = personFieldsIncludingCustomFields.find((f) => f.name === key); - if (key === 'merge') { - return ( -

- - Fusion avec : "{value.name}" - - - Identifiant: "{value._id}" - -

- ); - } - if (key === 'assignedTeams') { - return ( -

- {personField?.label} : - "{(value.oldValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" - - "{(value.newValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" -

- ); - } - if (key === 'outOfActiveListReasons') { - if (!value.newValue.length) return null; - return ( -

- {personField?.label}: - {value.newValue.join(', ')} -

- ); - } - if (key === 'outOfActiveList') { - return ( -

- {value.newValue === true ? 'Sortie de file active' : 'Réintégration dans la file active'} -

- ); - } - if (key === 'outOfActiveListDate') { - if (!value.newValue) return null; +
+

Historique

+
+ {!history?.length ? ( +
+

Cette personne n'a pas encore d'historique.

+

+ Lorsqu'une personne est modifiée, les changements sont enregistrés dans un historique, +
+ que vous pourrez ainsi retrouver sur cette page. +

+
+ ) : ( + + + + + + + + + + {history.map((h) => { + return ( + + + + - - ); - })} - -
DateUtilisateurDonnée
{dayjsInstance(h.date).format('DD/MM/YYYY HH:mm')} + + + {Object.entries(h.data).map(([key, value]) => { + const personField = personFieldsIncludingCustomFields.find((f) => f.name === key); + if (key === 'merge') { + return ( +

+ + Fusion avec : "{value.name}" + + + Identifiant: "{value._id}" + +

+ ); + } + if (key === 'assignedTeams') { + return ( +

+ {personField?.label} : + "{(value.oldValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" + + "{(value.newValue || []).map((teamId) => teams.find((t) => t._id === teamId)?.name).join(', ')}" +

+ ); + } + if (key === 'outOfActiveListReasons') { + if (!value.newValue.length) return null; + return ( +

+ {personField?.label}: + {value.newValue.join(', ')} +

+ ); + } + if (key === 'outOfActiveList') { + return ( +

+ {value.newValue === true ? 'Sortie de file active' : 'Réintégration dans la file active'} +

+ ); + } + if (key === 'outOfActiveListDate') { + if (!value.newValue) return null; + return ( +

+ {formatDateWithFullMonth(value.newValue)} +

+ ); + } + return ( -

- {formatDateWithFullMonth(value.newValue)} +

+ {personField?.label || 'Champs personnalisé supprimé'} :
+ {JSON.stringify(value.oldValue || '')}{JSON.stringify(value.newValue)}

); - } - - return ( -

- {personField?.label || 'Champs personnalisé supprimé'} :
- {JSON.stringify(value.oldValue || '')}{JSON.stringify(value.newValue)} -

- ); - })} -
+ })} +
+ )}
); }