From 270b722a269d0cdacbd0aa068c1afdd626e82a11 Mon Sep 17 00:00:00 2001 From: Arnaud AMBROSELLI Date: Fri, 8 Sep 2023 16:34:42 +0200 Subject: [PATCH 1/2] feat: historique pour les traitements --- app/src/recoil/treatments.js | 10 ++ app/src/scenes/Persons/Treatment.js | 28 ++++- dashboard/src/recoil/treatments.ts | 11 ++ .../person/components/MedicalFilePrint.js | 1 + .../person/components/TreatmentModal.js | 104 +++++++++++++++++- dashboard/src/types/treatment.ts | 1 + 6 files changed, 147 insertions(+), 8 deletions(-) diff --git a/app/src/recoil/treatments.js b/app/src/recoil/treatments.js index 3507fede9..8fdadd289 100644 --- a/app/src/recoil/treatments.js +++ b/app/src/recoil/treatments.js @@ -10,6 +10,16 @@ export const treatmentsState = atom({ const encryptedFields = ['person', 'user', 'startDate', 'endDate', 'name', 'dosage', 'frequency', 'indication', 'documents', 'comments', 'history']; +export const allowedTreatmentFieldsInHistory = [ + { name: 'person', label: 'Personne suivie' }, + { name: 'name', label: "Nom de l'action" }, + { name: 'startDate', label: 'Faite le' }, + { name: 'endDate', label: 'Faite le' }, + { name: 'dosage', label: 'Faite le' }, + { name: 'frequency', label: 'Faite le' }, + { name: 'indication', label: 'Faite le' }, +]; + export const prepareTreatmentForEncryption = (treatment) => { try { if (!looseUuidRegex.test(treatment.person)) { diff --git a/app/src/scenes/Persons/Treatment.js b/app/src/scenes/Persons/Treatment.js index 61fbde295..ae7b564f0 100644 --- a/app/src/scenes/Persons/Treatment.js +++ b/app/src/scenes/Persons/Treatment.js @@ -8,7 +8,7 @@ import ScreenTitle from '../../components/ScreenTitle'; import InputLabelled from '../../components/InputLabelled'; import Button from '../../components/Button'; import API from '../../services/api'; -import { prepareTreatmentForEncryption, treatmentsState } from '../../recoil/treatments'; +import { allowedTreatmentFieldsInHistory, prepareTreatmentForEncryption, treatmentsState } from '../../recoil/treatments'; import DateAndTimeInput from '../../components/DateAndTimeInput'; import DocumentsManager from '../../components/DocumentsManager'; import Spacer from '../../components/Spacer'; @@ -63,7 +63,7 @@ const Treatment = ({ navigation, route }) => { if (!startDate) return Alert.alert('Veuillez indiquer une date de début'); Keyboard.dismiss(); setPosting(true); - const body = prepareTreatmentForEncryption({ + const body = { name, dosage, frequency, @@ -74,9 +74,27 @@ const Treatment = ({ navigation, route }) => { documents, comments, user: treatmentDB?.user ?? user._id, - history: treatmentDB?.history ?? user._id, - }); - const treatmentResponse = isNew ? await API.post({ path: '/treatment', body }) : await API.put({ path: `/treatment/${treatmentDB._id}`, body }); + }; + + if (!isNew) { + const historyEntry = { + date: new Date(), + user: user._id, + data: {}, + }; + for (const key in body) { + if (!allowedTreatmentFieldsInHistory.map((field) => field.name).includes(key)) continue; + if (body[key] !== treatmentDB[key]) historyEntry.data[key] = { oldValue: treatmentDB[key], newValue: body[key] }; + } + if (!!Object.keys(historyEntry.data).length) { + const prevHistory = Array.isArray(treatmentDB.history) ? treatmentDB.history : []; + body.history = [...prevHistory, historyEntry]; + } + } + + const treatmentResponse = isNew + ? await API.post({ path: '/treatment', body: prepareTreatmentForEncryption(body) }) + : await API.put({ path: `/treatment/${treatmentDB._id}`, body: prepareTreatmentForEncryption(body) }); if (!treatmentResponse.ok) return false; if (isNew) { setAllTreatments((all) => [...all, treatmentResponse.decryptedData].sort((a, b) => new Date(b.startDate) - new Date(a.startDate))); diff --git a/dashboard/src/recoil/treatments.ts b/dashboard/src/recoil/treatments.ts index b3f2920ae..8a059f005 100644 --- a/dashboard/src/recoil/treatments.ts +++ b/dashboard/src/recoil/treatments.ts @@ -21,6 +21,17 @@ const encryptedFields: Array = [ 'indication', 'documents', 'comments', + 'history', +]; + +export const allowedTreatmentFieldsInHistory = [ + { name: 'person', label: 'Personne suivie' }, + { name: 'name', label: 'Nom du traitement' }, + { name: 'startDate', label: 'Date de début' }, + { name: 'endDate', label: 'Date de fin' }, + { name: 'dosage', label: 'Dosage' }, + { name: 'frequency', label: 'Fréquence' }, + { name: 'indication', label: 'Indication' }, ]; export const prepareTreatmentForEncryption = (treatment: TreatmentInstance, { checkRequiredFields = true } = {}) => { diff --git a/dashboard/src/scenes/person/components/MedicalFilePrint.js b/dashboard/src/scenes/person/components/MedicalFilePrint.js index 26993cb5e..d48bb4834 100644 --- a/dashboard/src/scenes/person/components/MedicalFilePrint.js +++ b/dashboard/src/scenes/person/components/MedicalFilePrint.js @@ -117,6 +117,7 @@ export function MedicalFilePrint({ person }) { 'createdAt', 'person', 'organisation', + 'history', ]; return (
diff --git a/dashboard/src/scenes/person/components/TreatmentModal.js b/dashboard/src/scenes/person/components/TreatmentModal.js index f5cb7c8e4..99d23d957 100644 --- a/dashboard/src/scenes/person/components/TreatmentModal.js +++ b/dashboard/src/scenes/person/components/TreatmentModal.js @@ -4,9 +4,9 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useHistory, useLocation } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; import { organisationState, userState } from '../../../recoil/auth'; -import { outOfBoundariesDate } from '../../../services/date'; +import { dayjsInstance, outOfBoundariesDate } from '../../../services/date'; import API from '../../../services/api'; -import { prepareTreatmentForEncryption, treatmentsState } from '../../../recoil/treatments'; +import { allowedTreatmentFieldsInHistory, prepareTreatmentForEncryption, treatmentsState } from '../../../recoil/treatments'; import DatePicker from '../../../components/DatePicker'; import { CommentsModule } from '../../../components/CommentsGeneric'; import { ModalContainer, ModalBody, ModalFooter, ModalHeader } from '../../../components/tailwind/Modal'; @@ -16,6 +16,7 @@ import CustomFieldDisplay from '../../../components/CustomFieldDisplay'; import UserName from '../../../components/UserName'; import { DocumentsModule } from '../../../components/DocumentsGeneric'; import TabsNav from '../../../components/tailwind/TabsNav'; +import PersonName from '../../../components/PersonName'; export default function TreatmentModal() { const treatmentsObjects = useRecoilValue(itemsGroupedByTreatmentSelector); @@ -84,6 +85,7 @@ function TreatmentContent({ onClose, treatment, personId }) { return { documents: [], comments: [], + history: [], ...treatment, }; } @@ -100,6 +102,7 @@ function TreatmentContent({ onClose, treatment, personId }) { organisation: organisation._id, documents: [], comments: [], + history: [], }; }, [treatment, user, personId, organisation]); const [activeTab, setActiveTab] = useState('Informations'); @@ -138,6 +141,23 @@ function TreatmentContent({ onClose, treatment, personId }) { toast.error('La date de fin de traitement est hors limites (entre 1900 et 2100)'); return false; } + + if (!isNewTreatment) { + const historyEntry = { + date: new Date(), + user: user._id, + data: {}, + }; + for (const key in body) { + if (!allowedTreatmentFieldsInHistory.map((field) => field.name).includes(key)) continue; + if (body[key] !== treatment[key]) historyEntry.data[key] = { oldValue: treatment[key], newValue: body[key] }; + } + if (!!Object.keys(historyEntry.data).length) { + const prevHistory = Array.isArray(treatment.history) ? treatment.history : []; + body.history = [...prevHistory, historyEntry]; + } + } + const treatmentResponse = isNewTreatment ? await API.post({ path: '/treatment', @@ -215,13 +235,15 @@ function TreatmentContent({ onClose, treatment, personId }) { 'Informations', `Documents ${data?.documents?.length ? `(${data.documents.length})` : ''}`, `Commentaires ${data?.comments?.length ? `(${data.comments.length})` : ''}`, + 'Historique', ]} onClick={(tab) => { if (tab.includes('Informations')) setActiveTab('Informations'); if (tab.includes('Documents')) setActiveTab('Documents'); if (tab.includes('Commentaires')) setActiveTab('Commentaires'); + if (tab.includes('Historique')) setActiveTab('Historique'); }} - activeTabIndex={['Informations', 'Documents', 'Commentaires'].findIndex((tab) => tab === activeTab)} + activeTabIndex={['Informations', 'Documents', 'Commentaires', 'Historique'].findIndex((tab) => tab === activeTab)} />
+
+ +
@@ -429,3 +457,73 @@ function TreatmentContent({ onClose, treatment, personId }) { ); } + +function TreatmentHistory({ treatment }) { + const history = useMemo(() => [...(treatment?.history || [])].reverse(), [treatment?.history]); + + return ( +
+ {!history?.length ? ( +
+

Ce traitement n'a pas encore d'historique.

+

+ Lorsqu'un traitement est modifié, 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 treatmentField = allowedTreatmentFieldsInHistory.find((f) => f.name === key); + + if (key === 'person') { + return ( +

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

+ ); + } + + return ( +

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

+ ); + })} +
+ )} +
+ ); +} diff --git a/dashboard/src/types/treatment.ts b/dashboard/src/types/treatment.ts index d3e398601..2b2edc32c 100644 --- a/dashboard/src/types/treatment.ts +++ b/dashboard/src/types/treatment.ts @@ -17,4 +17,5 @@ export interface TreatmentInstance { comments: any[]; createdAt: Date; updatedAt: Date; + history: any[]; } From bd68b229da879a1d860558868e4e922fb037447f Mon Sep 17 00:00:00 2001 From: Arnaud AMBROSELLI Date: Fri, 8 Sep 2023 16:52:39 +0200 Subject: [PATCH 2/2] test --- .../person/components/MedicalFilePrint.js | 1 + .../person/components/TreatmentModal.js | 2 +- e2e/treatment_crud.spec.ts | 70 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 e2e/treatment_crud.spec.ts diff --git a/dashboard/src/scenes/person/components/MedicalFilePrint.js b/dashboard/src/scenes/person/components/MedicalFilePrint.js index d48bb4834..1237b8e0f 100644 --- a/dashboard/src/scenes/person/components/MedicalFilePrint.js +++ b/dashboard/src/scenes/person/components/MedicalFilePrint.js @@ -171,6 +171,7 @@ export function MedicalFilePrint({ person }) { 'withTime', 'personPopulated', 'userPopulated', + 'history', ]; return (
diff --git a/dashboard/src/scenes/person/components/TreatmentModal.js b/dashboard/src/scenes/person/components/TreatmentModal.js index 99d23d957..96432a760 100644 --- a/dashboard/src/scenes/person/components/TreatmentModal.js +++ b/dashboard/src/scenes/person/components/TreatmentModal.js @@ -142,7 +142,7 @@ function TreatmentContent({ onClose, treatment, personId }) { return false; } - if (!isNewTreatment) { + if (!isNewTreatment && !!treatment) { const historyEntry = { date: new Date(), user: user._id, diff --git a/e2e/treatment_crud.spec.ts b/e2e/treatment_crud.spec.ts new file mode 100644 index 000000000..d3ddfcb17 --- /dev/null +++ b/e2e/treatment_crud.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from "@playwright/test"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import "dayjs/locale/fr"; +import { nanoid } from "nanoid"; +import { populate } from "./scripts/populate-db"; +import { changeReactSelectValue, clickOnEmptyReactSelect, loginWith, logOut } from "./utils"; + +dayjs.extend(utc); +dayjs.locale("fr"); + +test.beforeAll(async () => { + await populate(); +}); + +test("Traitement", async ({ page }) => { + const personName = "Manu Chao"; + + await loginWith(page, "admin1@example.org"); + await page.getByRole("link", { name: "Personnes suivies" }).click(); + await page.getByRole("button", { name: "Créer une nouvelle personne" }).click(); + await page.getByLabel("Nom").click(); + await page.getByLabel("Nom").fill(personName); + await page.getByRole("button", { name: "Sauvegarder" }).click(); + await page.getByText("Création réussie !").click(); + await page.getByRole("button", { name: "Dossier Médical" }).click(); + + await page.getByRole("button", { name: "Ajouter un traitement" }).click(); + await page.getByPlaceholder("Amoxicilline").click(); + await page.getByPlaceholder("Amoxicilline").fill("Paracétamol"); + await page.getByRole("button", { name: "Informations" }).click(); + await page.getByRole("button", { name: "Sauvegarder" }).click(); + await page.getByText("Traitement créé !").click(); + await page.getByText("Paracétamol").click(); + await page + .getByRole("dialog", { name: "Traitement: Paracétamol (créée par User Admin Test - 1)" }) + .getByRole("button", { name: "Historique" }) + .click(); + await page.getByText("Ce traitement n'a pas encore d'historique.Lorsqu'un traitement est modifié, les ").click(); + await page.getByRole("button", { name: "Informations" }).click(); + await page.getByTitle("Modifier ce traitement - seul le créateur peut modifier un traitement").click(); + await page.getByPlaceholder("1mg").click(); + await page.getByPlaceholder("1mg").fill("3mg"); + await page.getByPlaceholder("1 fois par jour").click(); + await page.getByPlaceholder("1 fois par jour").fill("2 fois par jour"); + await page.getByPlaceholder("Angine").click(); + await page.getByPlaceholder("Angine").fill("Grosse toux"); + await page.getByRole("button", { name: "Sauvegarder" }).click(); + await page.getByText("Traitement mis à jour !").click(); + await page.getByText("Paracétamol - Grosse toux - 3mg - 2 fois par jour").click(); + await page + .getByRole("dialog", { name: "Traitement: Paracétamol (créée par User Admin Test - 1)" }) + .getByRole("button", { name: "Historique" }) + .click(); + await page.locator('[data-test-id="Dosage\\: \\"\\" ➔ \\"3mg\\""]').click(); + await page.locator('[data-test-id="Fréquence\\: \\"\\" ➔ \\"2 fois par jour\\""]').click(); + await page.locator('[data-test-id="Indication\\: \\"\\" ➔ \\"Grosse toux\\""]').click(); + await page.getByRole("button", { name: "Informations" }).click(); + await page.getByTitle("Modifier ce traitement - seul le créateur peut modifier un traitement").click(); + + page.once("dialog", (dialog) => { + expect(dialog.message()).toBe("Voulez-vous supprimer ce traitement ?"); + dialog.accept(); + }); + await page + .getByRole("dialog", { name: "Modifier le traitement: Paracétamol (créée par User Admin Test - 1)" }) + .getByRole("button", { name: "Supprimer" }) + .click(); + await page.getByText("Traitement supprimé !").click(); +});