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 actions, consultations, traitements et dossier médical #1648

Merged
merged 15 commits into from
Sep 14, 2023
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mano

![Mobile version](https://img.shields.io/badge/mobile%20app%20version-2.36.2-blue)
![Mobile version](https://img.shields.io/badge/mobile%20app%20version-2.37.0-blue)
[![Maintainability](https://api.codeclimate.com/v1/badges/223e4185a3e13f1ef5d0/maintainability)](https://codeclimate.com/github/SocialGouv/mano/maintainability)

Code source de [Mano](https://mano-app.fabrique.social.gouv.fr/), organisé en plusieurs services :
Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "api_mano",
"version": "1.283.8",
"mobileAppVersion": "2.36.2",
"mobileAppVersion": "2.37.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
23 changes: 23 additions & 0 deletions api/src/controllers/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,29 @@ router.put(
}
}

if (req.params.migrationName === "integrate-comments-in-actions-history") {
try {
z.array(z.string().regex(looseUuidRegex)).parse(req.body.commentIdsToDelete);
z.array(
z.object({
_id: z.string().regex(looseUuidRegex),
encrypted: z.string(),
encryptedEntityKey: z.string(),
})
).parse(req.body.actionsToUpdate);
} catch (e) {
const error = new Error(`Invalid request in reports-from-real-date-to-date-id migration: ${e}`);
error.status = 400;
throw error;
}
for (const _id of req.body.commentIdsToDelete) {
await Comment.destroy({ where: { _id, organisation: req.user.organisation }, transaction: tx });
}
for (const { _id, encrypted, encryptedEntityKey } of req.body.actionsToUpdate) {
await Action.update({ encrypted, encryptedEntityKey }, { where: { _id }, transaction: tx, paranoid: false });
}
}

organisation.set({
migrations: [...(organisation.migrations || []), req.params.migrationName],
migrating: false,
Expand Down
5 changes: 2 additions & 3 deletions api/src/middleware/versionCheck.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { VERSION, MINIMUM_DASHBOARD_VERSION } = require("../config");

const MINIMUM_MOBILE_APP_VERSION = [2, 35, 0];
const MINIMUM_MOBILE_APP_VERSION = [2, 37, 0];

module.exports = ({ headers: { version, platform } }, res, next) => {
if (platform === "website") return next();
Expand Down Expand Up @@ -28,8 +28,7 @@ module.exports = ({ headers: { version, platform } }, res, next) => {
inAppMessage: [
`Veuillez mettre à jour votre application\u00A0!`,
`Les fonctionnalités de cette nouvelle version sont\u00A0:
- Compatibilité avec les consultations par équipe
- Possibilité de rajouter des commentaires dans les consultations, les traitements et les dossiers médicaux`,
- Compatibilité de l 'historique des actions, consultations, traitements et dossier médical (seulement consultable sur navigateur)`,
[{ text: "Télécharger la dernière version", link: `https://mano-app.fabrique.social.gouv.fr/download?ts=${Date.now()}` }],
],
});
Expand Down
4 changes: 2 additions & 2 deletions app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 16
versionName "2.36.2"
versionCode 17
versionName "2.37.0"
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
Expand Down
4 changes: 2 additions & 2 deletions app/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "mano",
"displayName": "Mano",
"version": {
"buildNumber": 16,
"buildName": "2.36.2"
"buildNumber": 17,
"buildName": "2.37.0"
},
"bundle": {
"android": "com.mano",
Expand Down
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mano",
"version": "2.36.2",
"version": "2.37.0",
"private": true,
"scripts": {
"get-ip": "./get-ip.sh",
Expand Down
13 changes: 13 additions & 0 deletions app/src/recoil/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
50 changes: 49 additions & 1 deletion app/src/recoil/consultations.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { atom } from 'recoil';
import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils/regex';
import { capture } from '../services/sentry';
import { Alert } from 'react-native';
import { organisationState } from './auth';

export const consultationsState = atom({
key: 'consultationsState',
Expand All @@ -10,6 +11,53 @@ export const consultationsState = atom({

export const encryptedFields = ['name', 'type', 'person', 'user', 'teams', 'documents', 'comments', 'history'];

export const consultationFieldsSelector = selector({
key: 'consultationFieldsSelector',
get: ({ get }) => {
const organisation = get(organisationState);
return organisation?.consultations || [];
},
});

export const flattenedCustomFieldsConsultationsSelector = selector({
key: 'flattenedCustomFieldsConsultationsSelector',
get: ({ get }) => {
const customFieldsConsultationsSections = get(consultationFieldsSelector);
const customFieldsConsultations = [];
for (const section of customFieldsConsultationsSections) {
for (const field of section.fields) {
customFieldsConsultations.push(field);
}
}
return customFieldsConsultations;
},
});

/* Other utils selector */

export const consultationsFieldsIncludingCustomFieldsSelector = selector({
key: 'consultationsFieldsIncludingCustomFieldsSelector',
get: ({ get }) => {
const flattenedCustomFieldsConsultations = get(flattenedCustomFieldsConsultationsSelector);
return [
{ name: 'name', label: 'Nom' },
{ name: 'type', label: 'Type' },
{ name: 'onlyVisibleBy', label: 'Seulement visible par moi' },
{ name: 'person', label: 'Personne suivie' },
{ name: 'teams', label: ':Equipe(s) en charge' },
{ name: 'completedAt', label: 'Faite le' },
{ name: 'dueAt', label: 'À faire le' },
{ name: 'status', label: 'Status' },
...flattenedCustomFieldsConsultations.map((f) => {
return {
name: f.name,
label: f.label,
};
}),
];
},
});

export const prepareConsultationForEncryption = (customFieldsConsultations) => (consultation) => {
try {
if (!looseUuidRegex.test(consultation.person)) {
Expand Down
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
27 changes: 14 additions & 13 deletions app/src/scenes/Actions/Action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, allowedActionFieldsInHistory } from '../../recoil/actions';
import { useRecoilState, useRecoilValue } from 'recoil';
import { commentsState, prepareCommentForEncryption } from '../../recoil/comments';
import API from '../../services/api';
Expand Down Expand Up @@ -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),
Expand All @@ -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 } });
Expand Down Expand Up @@ -313,7 +314,7 @@ const Action = ({ navigation, route }) => {
setActions((actions) => [response.decryptedData, ...actions]);
await createReportAtDateIfNotExist(response.decryptedData.createdAt);

for (let c of comments.filter((c) => c.action === actionDB._id).filter((c) => !c.comment.includes('a changé le status'))) {
for (let c of comments.filter((c) => c.action === actionDB._id)) {
const body = {
comment: c.comment,
action: response.decryptedData._id,
Expand Down
26 changes: 24 additions & 2 deletions app/src/scenes/Persons/Consultation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import DocumentsManager from '../../components/DocumentsManager';
import Spacer from '../../components/Spacer';
import Label from '../../components/Label';
import ActionStatusSelect from '../../components/Selects/ActionStatusSelect';
import { consultationsState, encryptedFields, prepareConsultationForEncryption } from '../../recoil/consultations';
import {
consultationsFieldsIncludingCustomFieldsSelector,
consultationsState,
encryptedFields,
prepareConsultationForEncryption,
} from '../../recoil/consultations';
import ConsultationTypeSelect from '../../components/Selects/ConsultationTypeSelect';
import CustomFieldInput from '../../components/CustomFieldInput';
import { currentTeamState, organisationState, userState } from '../../recoil/auth';
Expand All @@ -40,6 +45,7 @@ const Consultation = ({ navigation, route }) => {
const user = useRecoilValue(userState);
const currentTeam = useRecoilValue(currentTeamState);
const person = route?.params?.personDB || route?.params?.person;
const consultationsFieldsIncludingCustomFields = useRecoilValue(consultationsFieldsIncludingCustomFieldsSelector);

const consultationDB = useMemo(() => {
if (route?.params?.consultationDB?._id) {
Expand Down Expand Up @@ -137,12 +143,28 @@ const Consultation = ({ navigation, route }) => {
} else {
consultationToSave.completedAt = null;
}

if (!isNew) {
const historyEntry = {
date: new Date(),
user: user._id,
data: {},
};
for (const key in consultationToSave) {
if (!consultationsFieldsIncludingCustomFields.map((field) => field.name).includes(key)) continue;
if (consultationToSave[key] !== consultationDB[key]) {
historyEntry.data[key] = { oldValue: consultationDB[key], newValue: consultationToSave[key] };
}
}
if (!!Object.keys(historyEntry.data).length) consultationToSave.history = [...(consultationDB.history || []), historyEntry];
}

const body = prepareConsultationForEncryption(organisation.consultations)({
...consultationToSave,
teams: isNew ? [currentTeam._id] : consultationToSave.teams,
_id: consultationDB?._id,
});
console.log('body', body);

const consultationResponse = isNew
? await API.post({ path: '/consultation', body })
: await API.put({ path: `/consultation/${consultationDB._id}`, body });
Expand Down
16 changes: 15 additions & 1 deletion app/src/scenes/Persons/MedicalFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ const MedicalFile = ({ navigation, person, personDB, onUpdatePerson, updating, e

const onUpdateRequest = async (latestMedicalFile) => {
if (!latestMedicalFile) latestMedicalFile = medicalFile;

const historyEntry = {
date: new Date(),
user: user._id,
data: {},
};
for (const key in latestMedicalFile) {
if (!customFieldsMedicalFile.map((field) => field.name).includes(key)) continue;
if (latestMedicalFile[key] !== medicalFileDB[key]) {
historyEntry.data[key] = { oldValue: medicalFileDB[key], newValue: latestMedicalFile[key] };
}
}
if (!!Object.keys(historyEntry.data).length) latestMedicalFile.history = [...(medicalFileDB.history || []), historyEntry];

const response = await API.put({
path: `/medical-file/${medicalFileDB._id}`,
body: prepareMedicalFileForEncryption(customFieldsMedicalFile)({ ...medicalFileDB, ...latestMedicalFile }),
Expand Down Expand Up @@ -309,7 +323,7 @@ const MedicalFile = ({ navigation, person, personDB, onUpdatePerson, updating, e
<ButtonsContainer>
<Button
caption={editable ? 'Mettre à jour' : 'Modifier'}
onPress={editable ? onUpdateRequest : onEdit}
onPress={() => (editable ? onUpdateRequest() : onEdit())}
disabled={editable ? isUpdateDisabled && isMedicalFileUpdateDisabled : false}
loading={updating}
/>
Expand Down
5 changes: 5 additions & 0 deletions app/src/scenes/Persons/Person.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const Person = ({ route, navigation }) => {
outOfActiveList: person.outOfActiveList || false,
outOfActiveListReasons: person.outOfActiveListReasons || [],
documents: person.documents || [],
history: person.history || [],
};
},
[flattenedCustomFieldsPersons]
Expand Down Expand Up @@ -145,6 +146,10 @@ const Person = ({ route, navigation }) => {
}
if (!!Object.keys(historyEntry.data).length) personToUpdate.history = [...(oldPerson.history || []), historyEntry];

console.log('personToUpdate.history', personToUpdate.history);
console.log('oldPerson.history', oldPerson.history);
console.log('historyEntry', historyEntry);

const response = await API.put({
path: `/person/${personDB._id}`,
body: preparePersonForEncryption(personToUpdate),
Expand Down
Loading
Loading