Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: historique pour les traitements #1651

Merged
merged 2 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/recoil/treatments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
28 changes: 23 additions & 5 deletions app/src/scenes/Persons/Treatment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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)));
Expand Down
11 changes: 11 additions & 0 deletions dashboard/src/recoil/treatments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const encryptedFields: Array<keyof TreatmentInstance> = [
'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 } = {}) => {
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/scenes/person/components/MedicalFilePrint.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function MedicalFilePrint({ person }) {
'createdAt',
'person',
'organisation',
'history',
];
return (
<div key={c._id} className="tw-mb-8">
Expand Down Expand Up @@ -170,6 +171,7 @@ export function MedicalFilePrint({ person }) {
'withTime',
'personPopulated',
'userPopulated',
'history',
];
return (
<div key={c._id} className="tw-mb-8">
Expand Down
104 changes: 101 additions & 3 deletions dashboard/src/scenes/person/components/TreatmentModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -84,6 +85,7 @@ function TreatmentContent({ onClose, treatment, personId }) {
return {
documents: [],
comments: [],
history: [],
...treatment,
};
}
Expand All @@ -100,6 +102,7 @@ function TreatmentContent({ onClose, treatment, personId }) {
organisation: organisation._id,
documents: [],
comments: [],
history: [],
};
}, [treatment, user, personId, organisation]);
const [activeTab, setActiveTab] = useState('Informations');
Expand Down Expand Up @@ -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 && !!treatment) {
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',
Expand Down Expand Up @@ -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)}
/>
<form
id="add-treatment-form"
Expand Down Expand Up @@ -378,6 +400,12 @@ function TreatmentContent({ onClose, treatment, personId }) {
}}
/>
</div>
<div
className={['tw-flex tw-h-[50vh] tw-w-full tw-flex-col tw-gap-4 tw-overflow-y-auto', activeTab !== 'Historique' && 'tw-hidden']
.filter(Boolean)
.join(' ')}>
<TreatmentHistory treatment={treatment} />
</div>
</div>
</ModalBody>
<ModalFooter>
Expand Down Expand Up @@ -429,3 +457,73 @@ function TreatmentContent({ onClose, treatment, personId }) {
</>
);
}

function TreatmentHistory({ treatment }) {
const history = useMemo(() => [...(treatment?.history || [])].reverse(), [treatment?.history]);

return (
<div>
{!history?.length ? (
<div className="tw-py-10 tw-text-center tw-text-gray-300">
<p className="tw-text-lg tw-font-bold">Ce traitement n'a pas encore d'historique.</p>
<p className="tw-mt-2 tw-text-sm">
Lorsqu'un traitement est modifié, les changements sont enregistrés dans un historique,
<br />
que vous pourrez ainsi retrouver sur cette page.
</p>
</div>
) : (
<table className="table table-striped table-bordered">
<thead>
<tr className="tw-cursor-default">
<th>Date</th>
<th>Utilisateur</th>
<th>Donnée</th>
</tr>
</thead>
<tbody className="small">
{history.map((h) => {
return (
<tr key={h.date} className="tw-cursor-default">
<td>{dayjsInstance(h.date).format('DD/MM/YYYY HH:mm')}</td>
<td>
<UserName id={h.user} />
</td>
<td className="tw-max-w-prose">
{Object.entries(h.data).map(([key, value]) => {
const treatmentField = allowedTreatmentFieldsInHistory.find((f) => f.name === key);

if (key === 'person') {
return (
<p key={key}>
{treatmentField?.label} : <br />
<code>
<PersonName item={{ person: value.oldValue }} />
</code>{' '}
➔{' '}
<code>
<PersonName item={{ person: value.newValue }} />
</code>
</p>
);
}

return (
<p
key={key}
data-test-id={`${treatmentField?.label}: ${JSON.stringify(value.oldValue || '')} ➔ ${JSON.stringify(value.newValue)}`}>
{treatmentField?.label} : <br />
<code>{JSON.stringify(value.oldValue || '')}</code> ➔ <code>{JSON.stringify(value.newValue)}</code>
</p>
);
})}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
);
}
1 change: 1 addition & 0 deletions dashboard/src/types/treatment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface TreatmentInstance {
comments: any[];
createdAt: Date;
updatedAt: Date;
history: any[];
}
70 changes: 70 additions & 0 deletions e2e/treatment_crud.spec.ts
Original file line number Diff line number Diff line change
@@ -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, "[email protected]");
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();
});
Loading