diff --git a/dashboard/src/components/Card.js b/dashboard/src/components/Card.js
index 60f4868f1b..9dc13af47e 100644
--- a/dashboard/src/components/Card.js
+++ b/dashboard/src/components/Card.js
@@ -1,7 +1,7 @@
import React from 'react';
import HelpButtonAndModal from './HelpButtonAndModal';
-const Card = ({ title, count, unit, children, countId, dataTestId, help, onClick = null }) => {
+const Card = ({ title, count, unit, children, countId, dataTestId, help = null, onClick = null }) => {
const Component = !!onClick ? 'button' : 'div';
const props = !!onClick ? { onClick, type: 'button', name: 'card', className: 'button-cancel' } : {};
return (
diff --git a/dashboard/src/components/EvolutiveStatsViewer.tsx b/dashboard/src/components/EvolutiveStatsViewer.tsx
new file mode 100644
index 0000000000..ff3c99a1ad
--- /dev/null
+++ b/dashboard/src/components/EvolutiveStatsViewer.tsx
@@ -0,0 +1,30 @@
+export default function EvolutiveStatsViewer() {
+ return (
+
+
+
Au 31/12/2023
+
+
45
+
des personnes SDF
+
+
+
+
+
45%
+
+ des “Sans” Couverture Médicale au 31/01/2022
+
+ ont évolué vers “AME” au 01/02/2023
+
+
+
+
+
Au 31/12/2023
+
+
45
+
des personnes SDF
+
+
+
+ );
+}
diff --git a/dashboard/src/recoil/persons.ts b/dashboard/src/recoil/persons.ts
index 5fc470e7c2..9207b2ba9e 100644
--- a/dashboard/src/recoil/persons.ts
+++ b/dashboard/src/recoil/persons.ts
@@ -4,7 +4,9 @@ import { organisationState } from './auth';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
import type { PersonInstance } from '../types/person';
-import type { PredefinedField, CustomField } from '../types/field';
+import type { PredefinedField, CustomField, CustomOrPredefinedField } from '../types/field';
+import type { EvolutiveStatsPersonFields, EvolutiveStatOption, EvolutiveStatDateYYYYMMDD } from '../types/evolutivesStats';
+import { dayjsInstance } from '../services/date';
const collectionName = 'person';
export const personsState = atom({
@@ -76,14 +78,16 @@ export const personFieldsIncludingCustomFieldsSelector = selector({
return [
...personFields,
...[...fieldsPersonsCustomizableOptions, ...flattenedCustomFieldsPersons].map((f) => {
- return {
+ const field: CustomOrPredefinedField = {
name: f.name,
type: f.type,
label: f.label,
encrypted: true,
importable: true,
- options: f.options || null,
+ options: f.options || undefined,
+ filterable: true,
};
+ return field;
}),
];
},
@@ -97,6 +101,107 @@ export const allowedPersonFieldsInHistorySelector = selector({
},
});
+export const evolutiveStatsPersonSelector = selector({
+ key: 'evolutiveStatsPersonSelector',
+ get: ({ get }) => {
+ const allFields = get(personFieldsIncludingCustomFieldsSelector);
+ const fields = allFields.filter((f) => {
+ if (f.name === 'history') return false;
+ if (['text', 'textarea', 'number', 'date', 'date-with-time'].includes(f.type)) return false;
+ // remains 'yes-no' | 'enum' | 'multi-choice' | 'boolean'
+ return f.filterable;
+ });
+ const personsFieldsInHistoryObject: EvolutiveStatsPersonFields = {};
+ const persons = get(personsState);
+
+ function getValuesOptionsByField(field: CustomOrPredefinedField): Array {
+ if (!field) return [];
+ const current = fields.find((_field) => _field.name === field.name);
+ if (!current) return [];
+ if (['yes-no'].includes(current.type)) return ['Oui', 'Non', 'Non renseigné'];
+ if (['boolean'].includes(current.type)) return ['Oui', 'Non'];
+ if (current?.name === 'outOfActiveList') return current.options ?? ['Oui', 'Non'];
+ if (current?.options?.length) return [...current?.options, 'Non renseigné'];
+ return ['Non renseigné'];
+ }
+
+ // we take the years since the history began, let's say early 2023
+ const dates: Record = {};
+ let date = dayjsInstance('2023-01-01').format('YYYYMMDD');
+ const today = dayjsInstance().format('YYYYMMDD');
+ while (date !== today) {
+ dates[date] = 0;
+ date = dayjsInstance(date).add(1, 'day').format('YYYYMMDD');
+ }
+
+ for (const field of fields) {
+ const options = getValuesOptionsByField(field);
+ personsFieldsInHistoryObject[field.name] = {};
+ for (const option of options) {
+ personsFieldsInHistoryObject[field.name][option] = {
+ ...dates,
+ };
+ }
+ }
+
+ for (const person of persons) {
+ const minimumDate = dayjsInstance(person.followedSince || person.createdAt).format('YYYYMMDD');
+ let currentDate = today;
+ let currentPerson = structuredClone(person);
+ for (const field of fields) {
+ const value = currentPerson[field.name];
+ if (value == null || value === '') {
+ // we cover the case of undefined, null, empty string
+ continue;
+ }
+ personsFieldsInHistoryObject[field.name][value][currentDate]++;
+ }
+ const history = person.history;
+ if (!!history?.length) {
+ const reversedHistory = [...history].reverse();
+ for (const historyItem of reversedHistory) {
+ let historyDate = dayjsInstance(historyItem.date).format('YYYYMMDD');
+ while (currentDate !== historyDate) {
+ currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD');
+ for (const field of fields) {
+ const value = currentPerson[field.name];
+ if (value == null || value === '') {
+ // we cover the case of undefined, null, empty string
+ continue;
+ }
+ personsFieldsInHistoryObject[field.name][value][currentDate]++;
+ }
+ }
+ for (const historyChangeField of Object.keys(historyItem.data)) {
+ const oldValue = historyItem.data[historyChangeField].oldValue;
+ if (historyItem.data[historyChangeField].newValue !== currentPerson[historyChangeField]) {
+ capture(new Error('Incoherent history'), {
+ extra: {
+ person,
+ historyItem,
+ historyChangeField,
+ },
+ });
+ }
+ currentPerson[historyChangeField] = oldValue;
+ }
+ }
+ }
+ while (currentDate !== minimumDate) {
+ currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD');
+ for (const field of fields) {
+ const value = currentPerson[field.name];
+ if (value == null || value === '') {
+ // we cover the case of undefined, null, empty string
+ continue;
+ }
+ personsFieldsInHistoryObject[field.name][value][currentDate]++;
+ }
+ }
+ }
+ },
+});
+
export const filterPersonsBaseSelector = selector({
key: 'filterPersonsBaseSelector',
get: ({ get }) => {
diff --git a/dashboard/src/recoil/selectors.js b/dashboard/src/recoil/selectors.js
index 3d6d581645..f281403d75 100644
--- a/dashboard/src/recoil/selectors.js
+++ b/dashboard/src/recoil/selectors.js
@@ -1,5 +1,5 @@
import { currentTeamState, userState, usersState } from './auth';
-import { personsState } from './persons';
+import { allowedPersonFieldsForEvolutiveStatsSelector, personsState } from './persons';
import { placesState } from './places';
import { relsPersonPlaceState } from './relPersonPlace';
import { reportsState } from './reports';
@@ -98,9 +98,19 @@ export const itemsGroupedByPersonSelector = selector({
const personsObject = {};
const user = get(userState);
const usersObject = get(usersObjectSelector);
+
+ // const allPersonFieldsInHistory = get(allowedPersonFieldsForEvolutiveStatsSelector);
+ // const personsFieldsInHistoryObject = {};
+ // for (const field of allPersonFieldsInHistory) {
+ // const options = get(customFieldsObsSelector);
+ // personsFieldsInHistoryObject[field] = {
+ // __init: [],
+ // };
+ // }
+
for (const person of persons) {
originalPersonsObject[person._id] = { name: person.name, _id: person._id };
- personsObject[person._id] = {
+ const formattedPerson = {
...person,
followedSince: person.followedSince || person.createdAt,
userPopulated: usersObject[person.user],
@@ -114,9 +124,25 @@ export const itemsGroupedByPersonSelector = selector({
// This was causing a bug in the "person suivies" stats, where people who were not out of active list were counted as out of active list.
outOfActiveListDate: person.outOfActiveList ? person.outOfActiveListDate : null,
};
+ personsObject[person._id] = formattedPerson;
if (!person.history?.length) continue;
+ const updatedFields = {};
for (const historyEntry of person.history) {
+ // we suppose history is sorted by date ascending (oldest first)
+ // if history date is same as followedSince, we don't add it to interactions
+ if (dayjsInstance(historyEntry.date).format('YYYY-MM-DD') === dayjsInstance(formattedPerson.followedSince).format('YYYY-MM-DD')) continue;
personsObject[person._id].interactions.push(historyEntry.date);
+ // for (const field in Object.keys(historyEntry.data)) {
+ // const oldValue = historyEntry.data[field].oldValue;
+ // const newValue = historyEntry.data[field].newValue;
+ // if (!personsFieldsInHistoryObject[field]) continue;
+ // if (!updatedFields[field]) {
+ // personsFieldsInHistoryObject[field].__init.push(oldValue);
+ // updatedFields[field] = true;
+ // } else {
+ // updatedFields[field] = [];
+ // }
+ // }
}
}
const actions = Object.values(get(actionsWithCommentsSelector));
diff --git a/dashboard/src/scenes/stats/Blocks.js b/dashboard/src/scenes/stats/Blocks.js
index 663e0f50a6..bc12889c9f 100644
--- a/dashboard/src/scenes/stats/Blocks.js
+++ b/dashboard/src/scenes/stats/Blocks.js
@@ -1,9 +1,8 @@
-import React from 'react';
import { getDuration } from './utils';
import { capture } from '../../services/sentry';
import Card from '../../components/Card';
-export const Block = ({ data, title = 'Nombre de personnes suivies', help }) => (
+export const Block = ({ data, title = 'Nombre de personnes suivies', help = null }) => (
diff --git a/dashboard/src/scenes/stats/PersonsStats.js b/dashboard/src/scenes/stats/PersonsStats.js
index 8527f602f3..adef32fa17 100644
--- a/dashboard/src/scenes/stats/PersonsStats.js
+++ b/dashboard/src/scenes/stats/PersonsStats.js
@@ -19,6 +19,7 @@ import { dayjsInstance, formatDateWithFullMonth } from '../../services/date';
import CustomFieldDisplay from '../../components/CustomFieldDisplay';
import { groupsState } from '../../recoil/groups';
import EvolutiveStatsSelector from '../../components/EvolutiveStatsSelector';
+import EvolutiveStatsViewer from '../../components/EvolutiveStatsViewer';
export default function PersonStats({
title,
@@ -82,6 +83,7 @@ export default function PersonStats({
selection={evolutiveStatsIndicators}
onChange={setEvolutiveStatsIndicators}
/>
+
>
) : (
<>
diff --git a/dashboard/src/types/evolutivesStats.ts b/dashboard/src/types/evolutivesStats.ts
new file mode 100644
index 0000000000..9280ca8cd9
--- /dev/null
+++ b/dashboard/src/types/evolutivesStats.ts
@@ -0,0 +1,8 @@
+import type { CustomOrPredefinedField } from '../types/field';
+
+export type EvolutiveStatOption = string;
+export type EvolutiveStatDateYYYYMMDD = string;
+export type EvolutiveStatsPersonFields = Record<
+ CustomOrPredefinedField['name'],
+ Record>
+>;
diff --git a/dashboard/src/types/field.ts b/dashboard/src/types/field.ts
index 264d8b749b..b8ff01a109 100644
--- a/dashboard/src/types/field.ts
+++ b/dashboard/src/types/field.ts
@@ -26,6 +26,8 @@ export interface PredefinedField {
filterable?: boolean;
}
+export interface CustomOrPredefinedField extends PredefinedField {}
+
export interface CustomFieldsGroup {
name: string;
fields: CustomField[];
diff --git a/dashboard/src/types/history.ts b/dashboard/src/types/history.ts
deleted file mode 100644
index adcb96006a..0000000000
--- a/dashboard/src/types/history.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { UUIDV4 } from './uuid';
-
-export interface ElementHistory {
- date: Date;
- user: UUIDV4;
- data: HistoryData;
-}
-
-interface HistoryData {
- oldValue: T;
- newValue: T;
-}
diff --git a/dashboard/src/types/person.ts b/dashboard/src/types/person.ts
index d51d598fd5..6285fd2cb3 100644
--- a/dashboard/src/types/person.ts
+++ b/dashboard/src/types/person.ts
@@ -1,5 +1,4 @@
import type { UUIDV4 } from './uuid';
-import type { ElementHistory } from './history';
import type { Document, DocumentWithLinkedItem, Folder } from './document';
import type { UserInstance } from './user';
import type { GroupInstance } from './group';
@@ -7,12 +6,7 @@ import type { TreatmentInstance } from './treatment';
import type { ConsultationInstance } from './consultation';
import type { MedicalFileInstance } from './medicalFile';
-export interface PersonInstance {
- _id: UUIDV4;
- organisation: UUIDV4;
- createdAt: Date;
- updatedAt: Date;
- deletedAt?: Date;
+interface PersonInstanceBase {
outOfActiveList: boolean;
user: UUIDV4;
name: string;
@@ -23,13 +17,33 @@ export interface PersonInstance {
alertness?: boolean;
wanderingAt?: Date;
phone?: string;
- assignedTeams?: UUIDV4[]; // You might need to adjust this type based on what the actual values are.
+ assignedTeams?: UUIDV4[];
followedSince?: Date;
outOfActiveListDate?: Date;
outOfActiveListReasons?: string[];
documents?: Array;
- history?: ElementHistory[];
- [key: string]: any; // This allows for additional properties
+ [key: string]: any;
+}
+
+type PersonField = keyof PersonInstanceBase | string;
+
+export interface PersonInstance extends PersonInstanceBase {
+ _id: UUIDV4;
+ organisation: UUIDV4;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt?: Date;
+ history?: Array<{
+ date: Date;
+ user: UUIDV4;
+ data: Record<
+ PersonField,
+ {
+ oldValue: any;
+ newValue: any;
+ }
+ >;
+ }>;
}
export interface PersonPopulated extends PersonInstance {