Skip to content

Commit

Permalink
feat: evolutiveStatsPersonSelector
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud AMBROSELLI committed Feb 5, 2024
1 parent 5ca2ff5 commit 56af12f
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 30 deletions.
2 changes: 1 addition & 1 deletion dashboard/src/components/Card.js
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
30 changes: 30 additions & 0 deletions dashboard/src/components/EvolutiveStatsViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export default function EvolutiveStatsViewer() {
return (
<div className="tw-flex tw-w-full tw-justify-around">
<div className="tw-flex tw-basis-1/4 tw-flex-col tw-items-center tw-justify-end tw-gap-y-4">
<h5>Au 31/12/2023</h5>
<div className="tw-flex tw-flex-col tw-items-center tw-justify-around tw-rounded-lg tw-border tw-p-4">
<p className="tw-text-6xl tw-font-bold tw-text-main">45</p>
<p>des personnes SDF</p>
</div>
</div>
<div className="tw-flex tw-basis-1/2 tw-flex-col tw-items-center tw-justify-end tw-gap-y-4">
<div className="tw-flex tw-flex-col tw-items-center tw-justify-around tw-p-4">
<p className="tw-text-6xl tw-font-bold tw-text-main">45%</p>
<p className="tw-m-0 tw-text-center">
des “Sans” Couverture Médicale au 31/01/2022
<br />
ont évolué vers “AME” au 01/02/2023
</p>
</div>
</div>
<div className="tw-flex tw-basis-1/4 tw-flex-col tw-items-center tw-justify-end tw-gap-y-4">
<h5>Au 31/12/2023</h5>
<div className="tw-flex tw-flex-col tw-items-center tw-justify-around tw-rounded-lg tw-border tw-p-4">
<p className="tw-text-6xl tw-font-bold tw-text-main">45</p>
<p>des personnes SDF</p>
</div>
</div>
</div>
);
}
111 changes: 108 additions & 3 deletions dashboard/src/recoil/persons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PersonInstance[]>({
Expand Down Expand Up @@ -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;
}),
];
},
Expand All @@ -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<EvolutiveStatOption> {
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<EvolutiveStatDateYYYYMMDD, number> = {};
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 }) => {
Expand Down
30 changes: 28 additions & 2 deletions dashboard/src/recoil/selectors.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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],
Expand All @@ -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));
Expand Down
3 changes: 1 addition & 2 deletions dashboard/src/scenes/stats/Blocks.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div className="tw-px-4 tw-py-2 md:tw-basis-1/2 lg:tw-basis-1/3">
<Card title={title} count={Array.isArray(data) ? String(data.length) : data} help={help} />
</div>
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/scenes/stats/PersonsStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -82,6 +83,7 @@ export default function PersonStats({
selection={evolutiveStatsIndicators}
onChange={setEvolutiveStatsIndicators}
/>
<EvolutiveStatsViewer />
</>
) : (
<>
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/types/evolutivesStats.ts
Original file line number Diff line number Diff line change
@@ -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<EvolutiveStatOption, Record<EvolutiveStatDateYYYYMMDD, number>>
>;
2 changes: 2 additions & 0 deletions dashboard/src/types/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface PredefinedField {
filterable?: boolean;
}

export interface CustomOrPredefinedField extends PredefinedField {}

export interface CustomFieldsGroup {
name: string;
fields: CustomField[];
Expand Down
12 changes: 0 additions & 12 deletions dashboard/src/types/history.ts

This file was deleted.

34 changes: 24 additions & 10 deletions dashboard/src/types/person.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
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';
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;
Expand All @@ -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<Document | Folder>;
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 {
Expand Down

0 comments on commit 56af12f

Please sign in to comment.