From 5ca2ff5f08c5e9550443a57e165c94f6f790292e Mon Sep 17 00:00:00 2001 From: Arnaud AMBROSELLI Date: Fri, 2 Feb 2024 14:20:39 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20stats=20=C3=A9volutives=20-=20s=C3=A9le?= =?UTF-8?q?ction=20des=20indicateurs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{DatePicker.js => DatePicker.tsx} | 24 +- .../src/components/EvolutiveStatsSelector.tsx | 401 ++++++++++++++++++ .../{SelectCustom.js => SelectCustom.tsx} | 21 +- dashboard/src/recoil/persons.ts | 1 + dashboard/src/scenes/stats/PersonsStats.js | 10 +- dashboard/src/scenes/stats/index.js | 7 +- dashboard/src/types/field.ts | 15 + 7 files changed, 456 insertions(+), 23 deletions(-) rename dashboard/src/components/{DatePicker.js => DatePicker.tsx} (66%) create mode 100644 dashboard/src/components/EvolutiveStatsSelector.tsx rename dashboard/src/components/{SelectCustom.js => SelectCustom.tsx} (56%) diff --git a/dashboard/src/components/DatePicker.js b/dashboard/src/components/DatePicker.tsx similarity index 66% rename from dashboard/src/components/DatePicker.js rename to dashboard/src/components/DatePicker.tsx index bda49f17f0..d0d9c14083 100644 --- a/dashboard/src/components/DatePicker.js +++ b/dashboard/src/components/DatePicker.tsx @@ -1,23 +1,19 @@ import { dateForInputDate, dateFromInputDate, LEFT_BOUNDARY_DATE, RIGHT_BOUNDARY_DATE } from '../services/date'; -/** - * @typedef {Object} DatePickerProps - * @property {function} onChange - * @property {string} defaultValue - * @property {string} id - * @property {boolean} withTime - * @property {string} name - */ -/** - * @param {DatePickerProps} props - * @returns {JSX.Element} - */ -export default function DatePicker({ onChange, defaultValue, id, withTime = false, name = null }) { +interface DatePickerProps { + onChange: (e: { target: { name: string; value: Date | null } }) => void; + defaultValue: Date | null; + id: string; + withTime?: boolean; + name?: string; +} + +export default function DatePicker({ onChange, defaultValue, id, withTime = false, name = '' }: DatePickerProps): JSX.Element { return ( ; + +type IndicatorsBase = Array; + +interface EvolutiveStatsSelectorProps { + onChange: (selection: Selection, saveInURLParams: boolean) => void; + base: IndicatorsBase; + selection: Selection; + title?: string; + saveInURLParams?: boolean; +} + +const emptySelection = { field: null, type: null, fromValue: null, toValue: null }; +const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInURLParams = false }: EvolutiveStatsSelectorProps) => { + selection = !!selection.length ? selection : [emptySelection]; + const onAddIndicator = () => onChange([...selection, emptySelection], saveInURLParams); + const selectCustomOptions = base.map((_indicator) => ({ label: _indicator.label, value: _indicator.field })) || []; + + function getFilterOptionsByField(field: FilterField['field'] | null, base: IndicatorsBase, index: number) { + if (!field) return []; + let current = base.find((indicator) => indicator.field === field); + if (!current) { + onChange( + selection.filter((_f, i) => i !== index), + saveInURLParams + ); + return []; + } + if (['yes-no'].includes(current.type)) return ['Oui', 'Non', 'Non renseigné']; + if (['boolean'].includes(current.type)) return ['Oui', 'Non']; + if (current?.field === 'outOfActiveList') return current.options; + if (current?.options?.length) return [...current?.options, 'Non renseigné']; + return ['Non renseigné']; + } + + function getIndicatorValue(filterValue: IndicatorValue) { + if (typeof filterValue === 'object') { + // we have a date or a number + if (filterValue?.date != null) { + // we have a date + if (filterValue.comparator === 'unfilled') return 'Non renseigné'; + if (filterValue.comparator === 'before') return `Avant le ${dayjsInstance(filterValue.date).format('DD/MM/YYYY')}`; + if (filterValue.comparator === 'after') return `Après le ${dayjsInstance(filterValue.date).format('DD/MM/YYYY')}`; + if (filterValue.comparator === 'equals') return `Le ${dayjsInstance(filterValue.date).format('DD/MM/YYYY')}`; + return ''; + } + if (filterValue?.number != null) { + // we have a number + if (filterValue.comparator === 'unfilled') return 'Non renseigné'; + if (filterValue.comparator === 'between') return `Entre ${filterValue.number} et ${filterValue.number2}`; + if (filterValue.comparator === 'equals') return `Égal à ${filterValue.number}`; + if (filterValue.comparator === 'lower') return `Inférieur à ${filterValue.number}`; + if (filterValue.comparator === 'greater') return `Supérieur à ${filterValue.number}`; + } + return ''; + } + return filterValue; + } + + return ( + <> +
+ {title} +
    + {selection.map((indicator, index) => { + if (!indicator?.field) return null; + const current = base.find((_indicator) => _indicator.field === indicator.field); + if (!current) return null; + const indicatorFromValue = getIndicatorValue(indicator.fromValue); + if (!indicatorFromValue) return null; + const indicatorToValue = getIndicatorValue(indicator.toValue); + if (!indicatorToValue) return null; + return ( +
  • + {current.label}: de {indicatorFromValue} à {indicatorToValue} +
  • + ); + })} +
+
+
+
+
{title}
+
+
+ {selection.map((indicator, index) => { + // indicator: field, value, type + const indicatorValues = getFilterOptionsByField(indicator.field, base, index); + const onChangeFromValue = (newValue: any) => { + onChange( + selection.map((f, i) => (i === index ? { ...f, fromValue: newValue } : f)), + saveInURLParams + ); + }; + const onChangeToValue = (newValue: any) => { + onChange( + selection.map((f, i) => (i === index ? { ...f, toValue: newValue } : f)), + saveInURLParams + ); + }; + const onRemoveFilter = () => { + onChange( + selection.filter((_f, i) => i !== index), + saveInURLParams + ); + }; + + const value = selectCustomOptions.find((opt) => opt.value === indicator.field); + + return ( + +
+

{index === 0 ? 'Indicateur' : 'ET'}

+
+
+ { + const newField = base.find((_indicator) => _indicator.field === option?.value); + if (!newField) return; + onChange( + selection.map((_indicator, i) => + i === index ? { field: newField.field, fromValue: null, toValue: null, type: newField.type } : _indicator + ), + saveInURLParams + ); + }} + /> +
+
+

de

+
+
+ +
+
+

à

+
+
+ +
+
+ {!!selection.filter((_indicator) => Boolean(_indicator.field)).length && ( + + )} +
+
+ ); + })} +
+
+ +
+
+ + ); +}; + +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 à', + value: 'lower', + }, + { + label: 'Supérieur à', + value: 'greater', + }, + { + label: 'Égal à', + value: 'equals', + }, + { + label: 'Entre', + value: 'between', + }, + { + label: 'Non renseigné', + value: 'unfilled', + }, +]; + +interface ValueSelectorProps { + field: string | null; + indicatorValues: Array; + value: any; + onChangeValue: (newValue: any) => void; + base: IndicatorsBase; +} + +const ValueSelector = ({ field, indicatorValues, value, onChangeValue, base }: ValueSelectorProps) => { + const [comparator, setComparator] = useState(null); + if (!field) return <>; + const current = base.find((indicator) => indicator.field === field); + if (!current) return <>; + const { type, field: 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 ( +
+
+ opt.value === value?.comparator)} + isClearable={!value} + onChange={(option) => { + if (!option) return setComparator(null); + setComparator(option.value); + onChangeValue({ number: value?.number, comparator: option.value }); + }} + /> +
+ {value?.comparator !== 'unfilled' && ( +
+ { + onChangeValue({ number: e.target.value, number2: value?.number2, comparator }); + }} + /> +
+ )} + {value?.comparator === 'between' && ( + <> +
et
+
+ { + onChangeValue({ number2: e.target.value, number: value?.number, comparator }); + }} + /> +
+ + )} +
+ ); + } + + if (['enum', 'multi-choice'].includes(type) && name !== 'outOfActiveList') { + try { + return ( + ({ label: _value, value: _value }))} + value={value?.map((_value: any) => ({ label: _value, value: _value })) || []} + getOptionLabel={(f) => f.label} + getOptionValue={(f) => f.value} + onChange={(newValue) => onChangeValue(newValue?.map((option) => option.value))} + isClearable={!value?.length} + isMulti + components={{ + MultiValueContainer: (props) => { + if (props.selectProps?.value?.length <= 1) { + return ; + } + const lastValue = props.selectProps?.value?.[props.selectProps?.value?.length - 1]?.value; + const isLastValue = props?.data?.value === lastValue; + return ( + <> + + {!isLastValue && OU} + + ); + }, + }} + /> + ); + } catch (e) { + console.log(e); + } + return null; + } + + return ( + ({ label: _value, value: _value }))} + value={value ? { label: value, value } : null} + getOptionLabel={(f) => f.label} + getOptionValue={(f) => f.value} + onChange={(option) => onChangeValue(option?.value)} + isClearable={!value} + /> + ); +}; + +export default EvolutiveStatsSelector; diff --git a/dashboard/src/components/SelectCustom.js b/dashboard/src/components/SelectCustom.tsx similarity index 56% rename from dashboard/src/components/SelectCustom.js rename to dashboard/src/components/SelectCustom.tsx index 8b9fc831c9..75653a97e0 100644 --- a/dashboard/src/components/SelectCustom.js +++ b/dashboard/src/components/SelectCustom.tsx @@ -1,9 +1,17 @@ -import React from 'react'; import Select from 'react-select'; import CreatableSelect from 'react-select/creatable'; import { theme } from '../config'; +import type { GroupBase, Props } from 'react-select'; -const SelectCustom = ({ creatable, ...props }) => { +interface CustomProps = GroupBase