diff --git a/dashboard/src/components/EvolutiveStatsSelector.tsx b/dashboard/src/components/EvolutiveStatsSelector.tsx
index ddc17de3e..3e589ab5f 100644
--- a/dashboard/src/components/EvolutiveStatsSelector.tsx
+++ b/dashboard/src/components/EvolutiveStatsSelector.tsx
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import SelectCustom from './SelectCustom';
import { dayjsInstance } from '../services/date';
-import DatePicker from './DatePicker';
import type { FilterField } from '../types/field';
import type { IndicatorValue, IndicatorsSelection, IndicatorsBase } from '../types/evolutivesStats';
import { useRecoilValue } from 'recoil';
@@ -181,9 +180,14 @@ const EvolutiveStatsSelector = ({ onChange, selection, title = '', saveInURLPara
f.fieldName === null || f.toValue === null || f.fromValue === null) ? 'tw-opacity-0' : '',
+ ].join(' ')}
onClick={onAddIndicator}
- disabled={!!selection.find((f) => !f.fieldName)}>
+ // disabled={!!selection.find((f) => !f.fieldName)}
+ disabled>
+ Ajouter un indicateur
@@ -192,25 +196,6 @@ const EvolutiveStatsSelector = ({ onChange, selection, title = '', saveInURLPara
);
};
-const dateOptions = [
- {
- label: 'Avant',
- value: 'before',
- },
- {
- label: 'Après',
- value: 'after',
- },
- {
- label: 'Date exacte',
- value: 'equals',
- },
- {
- label: 'Non renseigné',
- value: 'unfilled',
- },
-];
-
const numberOptions = [
{
label: 'Inférieur à',
@@ -249,49 +234,6 @@ const ValueSelector = ({ fieldName, indicatorValues, value, onChangeValue, base
if (!current) return <>>;
const { type, name } = current;
- if (['text', 'textarea'].includes(type)) {
- return (
- {
- e.preventDefault();
- onChangeValue(e.target.value);
- }}
- />
- );
- }
-
- if (['date-with-time', 'date'].includes(type)) {
- return (
-
-
- opt.value === value?.comparator)}
- isClearable={!value}
- onChange={(option) => {
- if (!option) return setComparator(null);
- setComparator(option.value);
- onChangeValue({ date: value?.date, comparator: option.value });
- }}
- />
-
- {value?.comparator !== 'unfilled' && (
-
- onChangeValue({ date: date.target.value, comparator })}
- />
-
- )}
-
- );
- }
-
if (['number'].includes(type)) {
return (
@@ -357,7 +299,7 @@ const ValueSelector = ({ fieldName, indicatorValues, value, onChangeValue, base
getOptionLabel={(f) => f.label}
getOptionValue={(f) => f.value}
onChange={(newValue) => onChangeValue(newValue?.value)}
- isClearable={!value?.length}
+ isClearable
/>
);
} catch (e) {
@@ -373,7 +315,7 @@ const ValueSelector = ({ fieldName, indicatorValues, value, onChangeValue, base
getOptionLabel={(f) => f.label}
getOptionValue={(f) => f.value}
onChange={(option) => onChangeValue(option?.value)}
- isClearable={!value}
+ isClearable
/>
);
};
diff --git a/dashboard/src/components/EvolutiveStatsViewer.tsx b/dashboard/src/components/EvolutiveStatsViewer.tsx
index 2ab467ffd..281c64caa 100644
--- a/dashboard/src/components/EvolutiveStatsViewer.tsx
+++ b/dashboard/src/components/EvolutiveStatsViewer.tsx
@@ -18,24 +18,16 @@ interface EvolutiveStatsViewerProps {
export default function EvolutiveStatsViewer({ evolutiveStatsIndicators, period, persons }: EvolutiveStatsViewerProps) {
const startDate = period.startDate;
const endDate = period.endDate;
+ const indicatorsBase = useRecoilValue(evolutiveStatsIndicatorsBaseSelector);
const evolutiveStatsPerson = useRecoilValue(
evolutiveStatsPersonSelector({
persons,
startDate: period.startDate ? dayjsInstance(period.startDate).format('YYYY-MM-DD') : null,
+ evolutiveStatsIndicators,
})
);
- const indicatorsBase = useRecoilValue(evolutiveStatsIndicatorsBaseSelector);
if (!evolutiveStatsIndicators.length) return null;
const indicator = evolutiveStatsIndicators[0];
-
- console.log({
- evolutiveStatsPerson,
- startDate,
- endDate,
- evolutiveStatsIndicators,
- indicator,
- });
-
if (!indicator.fieldName) return null;
const startDateFormatted = dayjsInstance(startDate ?? startHistoryFeatureDate);
@@ -46,12 +38,18 @@ export default function EvolutiveStatsViewer({ evolutiveStatsIndicators, period,
const fieldStart = indicator.fromValue;
const fieldEnd = indicator.toValue;
- if (fieldStart == null || fieldEnd == null) {
+ const field = indicatorsBase.find((field) => field.name === indicator.fieldName);
+
+ console.log({
+ fieldStart,
+ fieldEnd,
+ evolutiveStatsPerson,
+ });
+ if (fieldEnd == null) {
return (
<>
- Évolution du champ {indicatorsBase.find((field) => field.name === indicator.fieldName)?.label} entre le{' '}
- {startDateFormatted.format('DD/MM/YYYY')} et le {endDateFormatted.format('DD/MM/YYYY')}
+ Évolution du champ {field?.label} entre le {startDateFormatted.format('DD/MM/YYYY')} et le {endDateFormatted.format('DD/MM/YYYY')}
-
+
Au {startDateFormatted.format('DD/MM/YYYY')}
-
+
{valueStart}
{fieldStart}
-
45%
+
{Math.round((valueEnd / valueStart) * 1000) / 10}%
- des “Sans” Couverture Médicale au 31/01/2022
+ des{' '}
+
+ {field?.label}: {fieldStart}
+ {' '}
+ au {startDateFormatted.format('DD/MM/YYYY')}
- ont évolué vers “AME” au 01/02/2023
+ ont évolué vers {fieldEnd} au {endDateFormatted.format('DD/MM/YYYY')}
-
+
Au {endDateFormatted.format('DD/MM/YYYY')}
-
+
@@ -111,7 +114,7 @@ function MyResponsiveStream({ indicator, evolutiveStatsPerson, startDateFormatte
const legend = [];
const fieldData = evolutiveStatsPerson[indicator.fieldName];
const daysDiff = endDateFormatted.diff(startDateFormatted, 'days');
- const spacing = Math.floor(Math.max(1, daysDiff / 12));
+ const spacing = Math.floor(Math.max(1, daysDiff / 6));
for (let i = 0; i < daysDiff; i += spacing) {
const date = startDateFormatted.add(i, 'days');
legend.push(date.format('DD/MM/YYYY'));
@@ -137,69 +140,104 @@ function MyResponsiveStream({ indicator, evolutiveStatsPerson, startDateFormatte
}, [startDateFormatted, endDateFormatted, evolutiveStatsPerson, indicator.fieldName]);
return (
-
-
chartData.legend[index],
- }}
- axisLeft={{
- // orient: 'left',
- tickSize: 5,
- tickPadding: 5,
- tickRotation: 0,
- legend: '',
- legendOffset: -40,
- }}
- curve="basis"
- enableGridX={true}
- enableGridY={true}
- offsetType="diverging"
- colors={{ scheme: 'set2' }}
- fillOpacity={0.85}
- borderColor={{ theme: 'background' }}
- dotSize={8}
- dotColor={{ from: 'color' }}
- dotBorderWidth={2}
- dotBorderColor={{
- from: 'color',
- modifiers: [['darker', 0.7]],
- }}
- legends={[
- {
- anchor: 'bottom-right',
- direction: 'column',
- translateX: 100,
- itemWidth: 80,
- itemHeight: 20,
- itemTextColor: '#999999',
- symbolSize: 12,
- symbolShape: 'circle',
- effects: [
- {
- on: 'hover',
- style: {
- itemTextColor: '#000000',
+
+
+ chartData.legend[index],
+ }}
+ axisLeft={{
+ // orient: 'left',
+ tickSize: 5,
+ tickPadding: 5,
+ tickRotation: 0,
+ truncateTickAt: 1,
+ legend: '',
+ legendOffset: -40,
+ format: (e) => (Math.floor(e) === e ? e : ''),
+ }}
+ curve="basis"
+ enableGridX={true}
+ enableGridY={false}
+ offsetType="diverging"
+ colors={{ scheme: 'set2' }}
+ fillOpacity={0.85}
+ borderColor={{ theme: 'background' }}
+ dotSize={8}
+ dotColor={{ from: 'color' }}
+ dotBorderWidth={2}
+ dotBorderColor={{
+ from: 'color',
+ modifiers: [['darker', 0.7]],
+ }}
+ legends={[
+ {
+ anchor: 'bottom-right',
+ direction: 'column',
+ translateX: 100,
+ itemWidth: 80,
+ itemHeight: 20,
+ itemTextColor: '#999999',
+ symbolSize: 12,
+ symbolShape: 'circle',
+ effects: [
+ {
+ on: 'hover',
+ style: {
+ itemTextColor: '#000000',
+ },
},
- },
- ],
- },
- ]}
- />
+ ],
+ },
+ ]}
+ />
+
+
+
+
+
+ Option
+ Au {startDateFormatted.format('DD/MM/YYYY')}
+ Au {endDateFormatted.format('DD/MM/YYYY')}
+ Différence
+ Différence (%)
+
+
+
+ {chartData.keys.map((option) => {
+ const startValue = chartData.data[0][option];
+ const endValue = chartData.data.at(-1)[option];
+ const diff = endValue - startValue;
+ const sign = diff > 0 ? '+' : '';
+ const percentDiff = startValue === 0 || endValue === 0 ? 0 : Math.round((diff / startValue) * 1000) / 10;
+ return (
+
+ {option}
+ {startValue}
+ {endValue}
+ {diff === 0 ? '' : `${sign}${diff}`}
+ {percentDiff === 0 ? '' : `${sign}${percentDiff}%`}
+
+ );
+ })}
+
+
+
);
}
diff --git a/dashboard/src/recoil/evolutiveStats.ts b/dashboard/src/recoil/evolutiveStats.ts
index fa4446e4b..49b1f96c2 100644
--- a/dashboard/src/recoil/evolutiveStats.ts
+++ b/dashboard/src/recoil/evolutiveStats.ts
@@ -2,6 +2,7 @@ import { selector, selectorFamily } from 'recoil';
import { capture } from '../services/sentry';
import type { PersonInstance } from '../types/person';
import type { CustomOrPredefinedField } from '../types/field';
+import type { IndicatorsSelection } from '../types/evolutivesStats';
import type { EvolutiveStatsPersonFields, EvolutiveStatOption, EvolutiveStatDateYYYYMMDD } from '../types/evolutivesStats';
import { dayjsInstance } from '../services/date';
import { personFieldsIncludingCustomFieldsSelector } from './persons';
@@ -35,71 +36,125 @@ export const evolutiveStatsIndicatorsBaseSelector = selector({
export const startHistoryFeatureDate = '2022-09-23';
+type FieldsMap = Record;
+
+function getValuesOptionsByField(field: CustomOrPredefinedField, fieldsMap: FieldsMap): Array {
+ if (!field) return [];
+ const current = fieldsMap[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é'].filter((option) => {
+ if (option.includes('Choisissez un genre')) return false;
+ return true;
+ });
+ }
+ return ['Non renseigné'];
+}
+
+function getValueByField(fieldName: CustomOrPredefinedField['name'], fieldsMap: FieldsMap, value: any): string | Array {
+ if (!fieldName) return '';
+ const current = fieldsMap[fieldName];
+ if (!current) return '';
+ if (['yes-no'].includes(current.type)) {
+ if (value === 'Oui') return 'Oui';
+ return 'Non';
+ }
+ if (['boolean'].includes(current.type)) {
+ if (value === true || value === 'Oui') return 'Oui';
+ return 'Non';
+ }
+ if (current?.name === 'outOfActiveList') {
+ if (value === true) return 'Oui';
+ return 'Non';
+ }
+ if (value == null || value === '') {
+ if (current.type === 'multi-choice') return [];
+ return 'Non renseigné'; // we cover the case of undefined, null, empty string
+ }
+ if (value.includes('Choisissez un genre')) return 'Non renseigné';
+ return value;
+}
+
+function getPersonSnapshotAtDate({
+ person,
+ snapshotDate,
+ fieldsMap,
+}: {
+ person: PersonInstance;
+ fieldsMap: FieldsMap;
+ snapshotDate: string; // YYYYMMDD
+}): PersonInstance | null {
+ let snapshot = structuredClone(person);
+ const followedSince = dayjsInstance(snapshot.followedSince || snapshot.createdAt).format('YYYYMMDD');
+ if (followedSince > snapshotDate) return null;
+ const history = snapshot.history;
+ if (!history?.length) return snapshot;
+ const reversedHistory = [...history].reverse();
+ for (const historyItem of reversedHistory) {
+ let historyDate = dayjsInstance(historyItem.date).format('YYYYMMDD');
+ if (historyDate < snapshotDate) return snapshot;
+ for (const historyChangeField of Object.keys(historyItem.data)) {
+ const oldValue = getValueByField(historyChangeField, fieldsMap, historyItem.data[historyChangeField].oldValue);
+ const historyNewValue = getValueByField(historyChangeField, fieldsMap, historyItem.data[historyChangeField].newValue);
+ const currentPersonValue = getValueByField(historyChangeField, fieldsMap, snapshot[historyChangeField]);
+ if (JSON.stringify(historyNewValue) !== JSON.stringify(currentPersonValue)) {
+ capture(new Error('Incoherent snapshot history'), {
+ extra: {
+ snapshot,
+ historyItem,
+ historyChangeField,
+ oldValue,
+ historyNewValue,
+ currentPersonValue,
+ },
+ });
+ }
+ if (oldValue === '') continue;
+ snapshot = {
+ ...snapshot,
+ [historyChangeField]: oldValue,
+ };
+ }
+ }
+ return snapshot;
+}
+
export const evolutiveStatsPersonSelector = selectorFamily({
key: 'evolutiveStatsPersonSelector',
get:
- ({ startDate, persons }: { startDate: string | null; persons: Array }) =>
+ ({
+ startDate,
+ persons,
+ evolutiveStatsIndicators,
+ }: {
+ startDate: string | null;
+ persons: Array;
+ evolutiveStatsIndicators: IndicatorsSelection;
+ }) =>
({ get }) => {
const now = Date.now();
const indicatorsBase = get(evolutiveStatsIndicatorsBaseSelector);
- const fieldsMap: Record = indicatorsBase.reduce((acc, field) => {
+ const fieldsMap: FieldsMap = indicatorsBase.reduce((acc, field) => {
acc[field.name] = field;
return acc;
- }, {} as Record);
+ }, {} as FieldsMap);
const personsFieldsInHistoryObject: EvolutiveStatsPersonFields = {};
- function getValuesOptionsByField(field: CustomOrPredefinedField): Array {
- if (!field) return [];
- const current = fieldsMap[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é'].filter((option) => {
- if (option.includes('Choisissez un genre')) return false;
- return true;
- });
- }
- return ['Non renseigné'];
- }
-
- function getValueByField(fieldName: CustomOrPredefinedField['name'], value: any): string | Array {
- if (!fieldName) return '';
- const current = fieldsMap[fieldName];
- if (!current) return '';
- if (['yes-no'].includes(current.type)) {
- if (value === 'Oui') return 'Oui';
- return 'Non';
- }
- if (['boolean'].includes(current.type)) {
- if (value === true || value === 'Oui') return 'Oui';
- return 'Non';
- }
- if (current?.name === 'outOfActiveList') {
- if (value === true) return 'Oui';
- return 'Non';
- }
- if (value == null || value === '') {
- if (current.type === 'multi-choice') return [];
- return 'Non renseigné'; // we cover the case of undefined, null, empty string
- }
- if (value.includes('Choisissez un genre')) return 'Non renseigné';
- return value;
- }
-
// we take the years since the history began, let's say early 2023
const dates: Record = {};
const minimumDateForEvolutiveStats = dayjsInstance(startDate ?? startHistoryFeatureDate).format('YYYYMMDD');
let date = minimumDateForEvolutiveStats;
const today = dayjsInstance().format('YYYYMMDD');
- while (date !== today) {
+ while (date <= today) {
dates[date] = 0;
date = dayjsInstance(date).add(1, 'day').format('YYYYMMDD');
}
for (const field of indicatorsBase) {
- const options = getValuesOptionsByField(field);
+ const options = getValuesOptionsByField(field, fieldsMap);
personsFieldsInHistoryObject[field.name] = {};
for (const option of options) {
personsFieldsInHistoryObject[field.name][option] = {
@@ -108,13 +163,23 @@ export const evolutiveStatsPersonSelector = selectorFamily({
}
}
- for (const [index, person] of Object.entries(persons)) {
+ const indicator = evolutiveStatsIndicators[0];
+ const indicatorFieldName = indicator?.fieldName;
+ if (typeof indicatorFieldName === 'string' && indicator?.fromValue) {
+ persons = persons.filter((p) => {
+ const snapshot = getPersonSnapshotAtDate({ person: p, snapshotDate: minimumDateForEvolutiveStats, fieldsMap });
+ if (!snapshot) return false;
+ return getValueByField(indicatorFieldName, fieldsMap, p[indicatorFieldName]).includes(indicator.fromValue);
+ });
+ }
+
+ for (const person of persons) {
const followedSince = dayjsInstance(person.followedSince || person.createdAt).format('YYYYMMDD');
const minimumDate = followedSince < minimumDateForEvolutiveStats ? minimumDateForEvolutiveStats : followedSince;
let currentDate = today;
let currentPerson = structuredClone(person);
for (const field of indicatorsBase) {
- const rawValue = getValueByField(field.name, currentPerson[field.name]);
+ const rawValue = getValueByField(field.name, fieldsMap, currentPerson[field.name]);
if (rawValue === '') continue;
const valueToLoop = Array.isArray(rawValue) ? rawValue : [rawValue];
for (const value of valueToLoop) {
@@ -139,7 +204,7 @@ export const evolutiveStatsPersonSelector = selectorFamily({
while (currentDate > historyDate && currentDate > minimumDate) {
currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD');
for (const field of indicatorsBase) {
- const rawValue = getValueByField(field.name, currentPerson[field.name]);
+ const rawValue = getValueByField(field.name, fieldsMap, currentPerson[field.name]);
if (rawValue === '') continue;
const valueToLoop = Array.isArray(rawValue) ? rawValue : [rawValue];
for (const value of valueToLoop) {
@@ -158,9 +223,9 @@ export const evolutiveStatsPersonSelector = selectorFamily({
}
}
for (const historyChangeField of Object.keys(historyItem.data)) {
- const oldValue = getValueByField(historyChangeField, historyItem.data[historyChangeField].oldValue);
- const historyNewValue = getValueByField(historyChangeField, historyItem.data[historyChangeField].newValue);
- const currentPersonValue = getValueByField(historyChangeField, currentPerson[historyChangeField]);
+ const oldValue = getValueByField(historyChangeField, fieldsMap, historyItem.data[historyChangeField].oldValue);
+ const historyNewValue = getValueByField(historyChangeField, fieldsMap, historyItem.data[historyChangeField].newValue);
+ const currentPersonValue = getValueByField(historyChangeField, fieldsMap, currentPerson[historyChangeField]);
if (JSON.stringify(historyNewValue) !== JSON.stringify(currentPersonValue)) {
capture(new Error('Incoherent history'), {
extra: {
@@ -175,14 +240,17 @@ export const evolutiveStatsPersonSelector = selectorFamily({
});
}
if (oldValue === '') continue;
- currentPerson[historyChangeField] = oldValue;
+ currentPerson = {
+ ...currentPerson,
+ [historyChangeField]: oldValue,
+ };
}
}
}
while (currentDate >= minimumDate) {
currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD');
for (const field of indicatorsBase) {
- const rawValue = getValueByField(field.name, currentPerson[field.name]);
+ const rawValue = getValueByField(field.name, fieldsMap, currentPerson[field.name]);
if (rawValue === '') continue;
const valueToLoop = Array.isArray(rawValue) ? rawValue : [rawValue];
for (const value of valueToLoop) {
diff --git a/dashboard/src/types/evolutivesStats.ts b/dashboard/src/types/evolutivesStats.ts
index 627dd11f7..b83abf9a4 100644
--- a/dashboard/src/types/evolutivesStats.ts
+++ b/dashboard/src/types/evolutivesStats.ts
@@ -1,4 +1,5 @@
import type { CustomOrPredefinedField } from '../types/field';
+import type { UUIDV4 } from './uuid';
export type EvolutiveStatOption = string;
export type EvolutiveStatDateYYYYMMDD = string;
@@ -6,6 +7,10 @@ export type EvolutiveStatsPersonFields = Record<
CustomOrPredefinedField['name'],
Record>
>;
+export type EvolutiveStatsOldestStatusPersonFields = Record<
+ CustomOrPredefinedField['name'],
+ Record>>
+>;
export type IndicatorValue = any;
export type Indicator = {