diff --git a/src/components/CreationCohort/DataList_Criteria.tsx b/src/components/CreationCohort/DataList_Criteria.tsx index 3fcf1d6d9..0182a6cc3 100644 --- a/src/components/CreationCohort/DataList_Criteria.tsx +++ b/src/components/CreationCohort/DataList_Criteria.tsx @@ -18,6 +18,9 @@ import services from 'services/aphp' import { CriteriaType, CriteriaTypeLabels } from 'types/requestCriterias' import { getConfig } from 'config' +import GHMForm2, { + form as ghmForm +} from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/GHMForm2' const criteriaList: () => CriteriaItemType[] = () => { const ODD_QUESTIONNAIRE = getConfig().features.questionnaires.enabled @@ -115,7 +118,8 @@ const criteriaList: () => CriteriaItemType[] = () => { title: CriteriaTypeLabels.CLAIM, color: '#0063AF', fontWeight: 'normal', - components: GhmForm, + formDefinition: ghmForm(), + components: GHMForm2, fetch: { ghmData: services.cohortCreation.fetchGhmData, encounterStatus: services.cohortCreation.fetchEncounterStatus diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/components/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/components/index.tsx new file mode 100644 index 000000000..de52a67d9 --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/components/index.tsx @@ -0,0 +1,100 @@ +import { FormLabel, Tooltip } from '@mui/material' +import React, { PropsWithChildren } from 'react' +import { + CriteriaDataType, + CriteriaFormItemView, + CriteriaFormItemViewProps, + CriteriaItem, + CriteriaItemType, + CriteriaSection, + DataTypeMapping +} from '../types' +import useStyles from '../style' +import { BlockWrapper } from 'components/ui/Layout' +import Collapse from 'components/ui/Collapse' +import { CriteriaLabel } from 'components/ui/CriteriaLabel' +import InfoIcon from '@mui/icons-material/Info' + +type CriteriaItemRuntimeProps = { + setError: (error?: string) => void + updateData: (data: T) => void + data: T + getValueSetOptions: CriteriaFormItemViewProps['getValueSetOptions'] + searchCode: CriteriaFormItemViewProps['searchCode'] + viewRenderers: { [key in CriteriaItemType]: CriteriaFormItemView } +} + +type CritieraItemProps> = CriteriaItemRuntimeProps & U + +export const CFLabel = (props: { label: string; tooltip?: string; altStyle?: boolean }) => { + const { label, tooltip, altStyle } = props + if (altStyle) { + return ( + + Fin de prise en charge + {tooltip && ( + + + + )} + + ) + } + return label +} + +export const CFItem = >(props: CritieraItemProps) => { + const { valueKey, updateData, data, setError, getValueSetOptions, searchCode, viewRenderers } = props + const View = viewRenderers[props.type] as CriteriaFormItemView + const fieldValue = data[valueKey] as DataTypeMapping[U['type']]['dataType'] + return ( + updateData({ ...data, [valueKey]: value })} + getValueSetOptions={getValueSetOptions} + searchCode={searchCode} + setError={setError} + /> + ) +} + +export const CFSection = ( + props: PropsWithChildren, 'items'> & { collapsed?: boolean }> +) => { + const { classes } = useStyles() + return props.title ? ( + + + {props.children} + + + ) : ( + <>{props.children} + ) +} + +export const CFItemWrapper = (props: PropsWithChildren<{ label?: string; info?: string }>) => { + const { classes } = useStyles() + return ( + + {props.label ? ( + + {props.info && ( + + + + )} + + ) : ( + '' + )} + {props.children} + + ) +} + +export default { CFItemWrapper, CFSection, CFLabel, CFItem } diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/index.tsx new file mode 100644 index 000000000..cf11b8dab --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/index.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react' +import CriteriaLayout from 'components/ui/CriteriaLayout' +import { + CriteriaData, + CriteriaForm as CriteriaFormDefinition, + CriteriaDataType, + CriteriaFormItemViewProps +} from './types' +import { CFItem, CFItemWrapper, CFSection } from './components' +import FORM_ITEM_RENDERER from './renderers' + +export type CriteriaFormRuntimeProps = { + goBack: () => void + data?: CriteriaData + updateData: (data: CriteriaData) => void + getValueSetOptions: CriteriaFormItemViewProps['getValueSetOptions'] + searchCode: CriteriaFormItemViewProps['searchCode'] +} + +type CriteriaFormProps = CriteriaFormDefinition & CriteriaFormRuntimeProps + +export default function CriteriaForm(props: CriteriaFormProps) { + const [criteriaData, setCriteriaData] = useState>(props.data || props.initialData) + const { goBack, updateData, label, warningAlert, getValueSetOptions, itemSections, errorMessages, onDataChange } = + props + const isEdition = !!props.data + const [error, setError] = useState() + + useEffect(() => { + onDataChange?.(criteriaData) + }, [criteriaData, onDataChange]) + + return ( + setCriteriaData({ ...criteriaData, title })} + isEdition={isEdition} + goBack={goBack} + onSubmit={() => updateData(criteriaData)} + disabled={error !== undefined} + isInclusive={criteriaData.isInclusive} + onChangeIsInclusive={(isInclusive) => setCriteriaData({ ...criteriaData, isInclusive: isInclusive })} + infoAlert={['Tous les éléments des champs multiples sont liés par une contrainte OU']} + warningAlert={warningAlert} + errorAlert={error ? [errorMessages[error]] : undefined} + > + {itemSections.map((section, index) => ( + { + const value = criteriaData[item.valueKey] + return value === null || value === undefined || (Array.isArray(value) && value.length === 0) + })} + > + {section.items.map((item, index) => ( + + { + setCriteriaData({ ...criteriaData, ...newData }) + }, + setError + }} + /> + + ))} + + ))} + + ) +} diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/legacyFormAdapter.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/legacyFormAdapter.tsx new file mode 100644 index 000000000..3d01a2253 --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/legacyFormAdapter.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { CriteriaDrawerComponentProps, CriteriaItemDataCache } from 'types' +import { CriteriaData, CriteriaDataType } from './types' +import CriteriaForm from '.' +import { CriteriaForm as CriteriaFormDefinition } from './types' +import { LabelObject } from 'types/searchCriterias' +import { CriteriaDataKey, SelectedCriteriaType } from 'types/requestCriterias' +import { fetchValueSet } from 'services/aphp/callApi' + +export type LegacyAdapterProps = { + form: CriteriaFormDefinition + adapter: { + mapFromLegacyDataType: (legacyData: U, criteriaData: CriteriaItemDataCache) => CriteriaData + mapToLegacyDataType: (data: CriteriaData) => Omit + valueSetIdToKey: (valueSetId: string) => CriteriaDataKey | undefined + } +} + +/** + * Enable the use of the new CriteriaForm component with the legacy data format + * @param props contains the form definition and the adapter to convert the legacy data to the new data format + * @returns the legacy Drawer component with the new CriteriaForm component + */ +export default function withLegacyAdapter( + props: LegacyAdapterProps +) { + return (legacyProps: CriteriaDrawerComponentProps) => { + const { criteriaData, goBack, selectedCriteria, onChangeSelectedCriteria } = legacyProps + const runtimeProps = { + data: selectedCriteria + ? props.adapter.mapFromLegacyDataType(selectedCriteria as U, criteriaData) + : props.form.initialData, + updateData: (data: CriteriaData) => onChangeSelectedCriteria(props.adapter.mapToLegacyDataType(data) as U), + goBack, + getValueSetOptions: (valueSetId: string) => { + const dataKey = props.adapter.valueSetIdToKey(valueSetId) + if (dataKey === undefined) { + return [] as LabelObject[] + } + return (criteriaData.data[dataKey] as LabelObject[]) || [] + } + } + return ( + + fetchValueSet( + codeSystemUrl, + { valueSetTitle: 'Toute la hiérarchie', search: code, noStar: false }, + abortSignal + ) + } + /> + ) + } +} diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/renderers.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/renderers.tsx new file mode 100644 index 000000000..8f75b5c0d --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/renderers.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import { CriteriaFormItemView, CriteriaItemType } from './types' +import { CFLabel } from './components' +import CalendarRange from 'components/ui/Inputs/CalendarRange' +import { Autocomplete, TextField } from '@mui/material' +import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnit' +import OccurenceInput from 'components/ui/Inputs/Occurences' +import SearchbarWithCheck from 'components/ui/Inputs/SearchbarWithCheck' +import AsyncAutocomplete from 'components/ui/Inputs/AsyncAutocomplete' + +const FORM_ITEM_RENDERER: { [key in CriteriaItemType]: CriteriaFormItemView } = { + text: (props) => , + duration: (props) => ( + <> + + ) + } + value={!!props.value ? [props.value.start, props.value.end] : [null, null]} + onChange={(range, includeNull) => + props.updateData({ start: range[0] || null, end: range[1] || null, includeNull }) + } + onError={(isError) => props.setError(isError ? props.definition.errorType : undefined)} + includeNullValues={props.value?.includeNull} + onChangeIncludeNullValues={ + props.definition.withOptionIncludeNull + ? () => { + /* dummy TODO change CalendarRange to accept a boolean to activate the includeNull checkbox */ + } + : undefined + } + /> + + ), + autocomplete: (props) => { + return ( + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={props.value} + onChange={(e, value) => props.updateData(value)} + renderInput={(params) => } + /> + ) + }, + number: (props) => , + executiveUnit: (props) => ( + props.updateData(value)} + /> + ), + occurrence: (props) => ( + { + props.updateData({ value: newCount, comparator: newComparator }) + }} + withHierarchyInfo={props.definition.withHierarchyInfo} + /> + ), + boolean: (props) => , + textWithCheck: (props) => ( + props.updateData(value)} + placeholder={props.definition.placeholder} + onError={(isError) => props.setError(isError ? props.definition.errorType : undefined)} + /> + ), + codeSearch: (props) => { + return ( + props.searchCode(search, props.definition.valueSetId, signal)} + onChange={(value) => props.updateData(value)} + /> + ) + } +} + +export default FORM_ITEM_RENDERER diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/style.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/style.ts new file mode 100644 index 000000000..8765a8bcb --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/style.ts @@ -0,0 +1,53 @@ +import { makeStyles } from 'tss-react/mui' + +const useStyles = makeStyles()(() => ({ + root: { + display: 'flex', + flexDirection: 'column', + flex: '1 1 auto' + }, + actionContainer: { + display: 'flex', + alignItems: 'center', + height: 72, + padding: 20, + backgroundColor: '#317EAA', + color: 'white', + // Not default + marginBottom: 46 + }, + backButton: { color: 'white' }, + divider: { background: 'white' }, + titleLabel: { marginLeft: '1em' }, + formContainer: { + overflow: 'auto', + maxHeight: 'calc(100vh - 183px)' + }, + inputContainer: { + padding: '1em', + display: 'flex', + flex: '1 1 0%', + flexDirection: 'column' + }, + inputItem: { + margin: '1em', + width: 'calc(100% - 2em)' + }, + criteriaActionContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexWrap: 'wrap', + borderTop: '1px solid grey', + position: 'absolute', + width: '100%', + bottom: 0, + left: 0, + background: '#fff', + '& > button': { + margin: '12px 8px' + } + } +})) + +export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/types.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/types.ts new file mode 100644 index 000000000..f1936941a --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/types.ts @@ -0,0 +1,240 @@ +import { ReactNode } from 'react' +import { ScopeElement } from 'types' +import { Hierarchy } from 'types/hierarchy' +import { Comparators } from 'types/requestCriterias' +import { SourceType } from 'types/scope' +import { LabelObject } from 'types/searchCriterias' + +/********************************************************************************/ +/* Criteria Types */ +/********************************************************************************/ +// When adding a new criteria type you must complete the following steps: +// 1. **REQUIRED** Add the new type to the CriteriaItemType union type +// 2. **REQUIRED** create a new CriteriaItem type (with required props "type") and then add the new type to the CriteriaItems union type +// 3. Optionnaly create the new data type definition and add it to the DataType union type +// 5. **REQUIRED** Update the DataTypeMapping type with the new type mapping + +// List of criteria item types +export type CriteriaItemType = + | 'duration' + | 'text' + | 'autocomplete' + | 'number' + | 'executiveUnit' + | 'occurrence' + | 'boolean' + | 'textWithCheck' + | 'codeSearch' + +/****************************************************************/ +/* Criteria Data Types */ +/****************************************************************/ + +export type NewDurationRangeType = { + start: string | null + end: string | null + includeNull: boolean +} + +export type OccurenceDataType = { + value: number + comparator: Comparators +} + +// Union of all criteria data types +export type DataType = + | NewDurationRangeType + | string + | LabelObject[] + | number + | Hierarchy[] + | OccurenceDataType + | boolean + | null + +/****************************************************************/ +/* Criteria Item Definition Types */ +/****************************************************************/ + +type BaseCriteriaItem = { + label?: string + labelAltStyle?: boolean + info?: string + // these are used to display external label and info on top of the component + extraLabel?: string + extraInfo?: string +} + +type WithLabel = { + label: string +} + +type WithErrorType = { + errorType: string +} + +export type TextCriteriaItem = BaseCriteriaItem & { + type: 'text' +} + +export type NumberCriteriaItem = BaseCriteriaItem & { + type: 'number' +} + +export type BooleanCriteriaItem = BaseCriteriaItem & { + type: 'boolean' +} + +export type OccurrenceCriteriaItem = BaseCriteriaItem & + WithLabel & { + type: 'occurrence' + withHierarchyInfo?: boolean + } + +export type CalendarItem = BaseCriteriaItem & + WithErrorType & { + type: 'duration' + withOptionIncludeNull?: boolean + } + +export type AutoCompleteItem = BaseCriteriaItem & { + type: 'autocomplete' + noOptionsText: string + valueSetId: string +} + +export type CodeSearchItem = BaseCriteriaItem & { + type: 'codeSearch' + noOptionsText: string + valueSetId: string +} + +export type ExecutiveUnitItem = BaseCriteriaItem & + WithLabel & { + type: 'executiveUnit' + sourceType: SourceType + } + +export type TextWithCheckItem = BaseCriteriaItem & + WithErrorType & { + type: 'textWithCheck' + placeholder: string + } + +// Union of all criteria item types +export type CriteriaItems = + | CalendarItem + | AutoCompleteItem + | ExecutiveUnitItem + | TextWithCheckItem + | BooleanCriteriaItem + | TextCriteriaItem + | NumberCriteriaItem + | CodeSearchItem + | OccurrenceCriteriaItem + +/****************************************************************/ +/* Type Mapping */ +/****************************************************************/ + +type CriteriaTypeMapping = { + dataType: T + definition: U +} + +// Mapping of criteria item types to their respective data types and definitions +export type DataTypeMapping = { + duration: CriteriaTypeMapping + text: CriteriaTypeMapping + autocomplete: CriteriaTypeMapping + number: CriteriaTypeMapping + executiveUnit: CriteriaTypeMapping[], ExecutiveUnitItem> + occurrence: CriteriaTypeMapping + boolean: CriteriaTypeMapping + textWithCheck: CriteriaTypeMapping + codeSearch: CriteriaTypeMapping +} + +/********************************************************************************/ +/* Criteria Form Types */ +/********************************************************************************/ + +/****************************************************************/ +/* Criteria Form Data */ +/****************************************************************/ + +// Will replace SelectedCriteriaType +export type CriteriaDataType = { + [key: string]: DataType +} + +export type CriteriaData = { + id?: number + title: string + isInclusive: boolean + occurrence: OccurenceDataType + encounterStatus: LabelObject[] +} & T + +// helpers +export type WithOccurenceCriteriaDataType = { + startOccurrence: NewDurationRangeType | null +} + +export type WithEncounterDateDataType = { + encounterStartDate: NewDurationRangeType | null + encounterEndDate: NewDurationRangeType | null +} + +export type WithEncounterStatusDataType = { + encounterStatus: LabelObject[] + encounterService: Hierarchy[] | null +} + +/****************************************************************/ +/* Criteria Form Definition */ +/****************************************************************/ + +// TODO: will be used later in utils/cohortCreation.ts to build / unbuild the criteria data +export type BuildInfo = { + fhirKey: string + buildMethod: string +} + +// Criteria item definition +export type CriteriaItem = { + valueKey: keyof CriteriaData + build?: BuildInfo +} & CriteriaItems + +// Criteria section listing criteria items +export type CriteriaSection = { + title?: string + defaulCollapsed?: boolean + items: CriteriaItem[] +} + +// Criteria form definition +export type CriteriaForm = { + label: string + warningAlert?: ReactNode[] + initialData: CriteriaData + errorMessages: { [key: string]: string } + onDataChange?: (data: CriteriaData) => void + itemSections: CriteriaSection[] +} + +/****************************************************************/ +/* Criteria Item Render */ +/****************************************************************/ + +export type CriteriaFormItemViewProps = { + value: DataTypeMapping[T]['dataType'] + definition: DataTypeMapping[T]['definition'] + getValueSetOptions: (valueSetId: string) => LabelObject[] + searchCode: (code: string, codeSystemUrl: string, abortSignal: AbortSignal) => Promise + updateData: (value: DataTypeMapping[T]['dataType']) => void + setError: (error?: string) => void +} + +export type CriteriaFormItemView = React.FC> diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/utils.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/utils.ts new file mode 100644 index 000000000..78aec8614 --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/utils.ts @@ -0,0 +1,18 @@ +import { CriteriaItemDataCache } from 'types' +import { CriteriaDataKey } from 'types/requestCriterias' +import { LabelObject } from 'types/searchCriterias' + +export const mappingCriteria = ( + criteriaToMap: LabelObject[] | null, + key: CriteriaDataKey, + mapping: CriteriaItemDataCache +) => { + if (criteriaToMap) { + return criteriaToMap.map((criteria) => { + const mappedCriteria = mapping.data?.[key]?.find((c: LabelObject) => c?.id === criteria?.id) + return mappedCriteria + }) + } else { + return [] + } +} diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/GHMForm2.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/GHMForm2.tsx new file mode 100644 index 000000000..5ea82a5cd --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/GHMForm2.tsx @@ -0,0 +1,177 @@ +import React from 'react' +import { Comparators, CriteriaDataKey, CriteriaType, GhmDataType } from 'types/requestCriterias' +import withLegacyAdapter from '../CriteriaForm/legacyFormAdapter' +import { + CriteriaData, + CriteriaForm, + WithEncounterDateDataType, + WithEncounterStatusDataType, + WithOccurenceCriteriaDataType +} from '../CriteriaForm/types' +import { LabelObject } from 'types/searchCriterias' +import { Link } from '@mui/material' +import { SourceType } from 'types/scope' +import { getConfig } from 'config' +import { CriteriaItemDataCache } from 'types' +import { mappingCriteria } from '../CriteriaForm/utils' + +type NewGhmDataType = WithOccurenceCriteriaDataType & + WithEncounterDateDataType & + WithEncounterStatusDataType & { + code: LabelObject[] | null + } + +export const form: () => CriteriaForm = () => ({ + label: 'GHM', + initialData: { + title: 'Critères GHM', + isInclusive: true, + occurrence: { + value: 1, + comparator: Comparators.GREATER_OR_EQUAL + }, + encounterService: null, + encounterStatus: [], + startOccurrence: null, + encounterStartDate: null, + encounterEndDate: null, + code: [] + }, + warningAlert: [ +
+ Données actuellement disponibles : PMSI ORBIS. Pour plus d'informations sur les prochaines intégrations de + données, veuillez vous référer au tableau trimestriel de disponibilité des données disponible{' '} + + ici + +
+ ], + errorMessages: {}, + itemSections: [ + { + items: [ + { + valueKey: 'occurrence', + type: 'occurrence', + label: "Nombre d'occurences", + withHierarchyInfo: true + }, + { + valueKey: 'code', + type: 'codeSearch', + valueSetId: getConfig().features.claim.valueSets.claimHierarchy.url, + noOptionsText: 'Aucun GHM trouvé', + label: 'Code GHM' + }, + { + valueKey: 'encounterStatus', + type: 'autocomplete', + label: 'Statut de la visite associée', + valueSetId: getConfig().core.valueSets.encounterStatus.url, + noOptionsText: 'Aucun statut trouvé' + } + ] + }, + { + title: 'Options avancées', + defaulCollapsed: true, + items: [ + { + valueKey: 'encounterService', + label: 'Unité exécutrice', + type: 'executiveUnit', + sourceType: SourceType.GHM + }, + { + valueKey: 'encounterStartDate', + type: 'duration', + errorType: 'ADVANCED_INPUTS_ERROR', + label: 'Début de prise en charge', + labelAltStyle: true, + extraLabel: 'Prise en charge', + withOptionIncludeNull: true + }, + { + valueKey: 'encounterEndDate', + type: 'duration', + label: 'Fin de prise en charge', + labelAltStyle: true, + info: 'Ne concerne pas les consultations', + errorType: 'ADVANCED_INPUTS_ERROR', + withOptionIncludeNull: true + }, + { + valueKey: 'startOccurrence', + type: 'duration', + errorType: 'ADVANCED_INPUTS_ERROR', + extraLabel: 'Date de classement en GHM' + } + ] + } + ] +}) + +export default withLegacyAdapter({ + form: form(), + adapter: { + mapFromLegacyDataType: (legacyData: GhmDataType, criteriaDataCache: CriteriaItemDataCache) => ({ + id: legacyData.id, + title: legacyData.title, + isInclusive: legacyData.isInclusive || true, + occurrence: { + value: legacyData.occurrence || 1, + comparator: legacyData.occurrenceComparator || Comparators.GREATER_OR_EQUAL + }, + encounterService: legacyData.encounterService || null, + encounterStatus: legacyData.encounterStatus, + encounterStartDate: + !legacyData.encounterStartDate[0] && !legacyData.encounterStartDate[1] + ? null + : { + start: legacyData.encounterStartDate[0] || null, + end: legacyData.encounterStartDate[1] || null, + includeNull: legacyData.includeEncounterStartDateNull || false + }, + encounterEndDate: + !legacyData.encounterEndDate[0] && !legacyData.encounterEndDate[1] + ? null + : { + start: legacyData.encounterEndDate[0] || null, + end: legacyData.encounterEndDate[1] || null, + includeNull: legacyData.includeEncounterEndDateNull || false + }, + startOccurrence: + !legacyData.startOccurrence[0] && !legacyData.startOccurrence[1] + ? null + : { + start: legacyData.startOccurrence[0] || null, + end: legacyData.startOccurrence[1] || null, + includeNull: false + }, + endOccurrence: legacyData.endOccurrence || [null, null], + code: mappingCriteria(legacyData.code, CriteriaDataKey.GHM_DATA, criteriaDataCache) + }), + mapToLegacyDataType: (data: CriteriaData) => ({ + id: data.id, + type: CriteriaType.CLAIM, + title: data.title, + isInclusive: data.isInclusive, + occurrence: data.occurrence.value, + startOccurrence: [data.startOccurrence?.start, data.startOccurrence?.end], + occurrenceComparator: data.occurrence.comparator, + encounterStatus: data.encounterStatus, + encounterService: data.encounterService || undefined, + encounterStartDate: [data.encounterStartDate?.start, data.encounterStartDate?.end], + encounterEndDate: [data.encounterEndDate?.start, data.encounterEndDate?.end], + includeEncounterStartDateNull: data.encounterStartDate?.includeNull, + includeEncounterEndDateNull: data.encounterEndDate?.includeNull, + label: undefined, + code: data.code + }), + valueSetIdToKey: () => undefined + } +}) diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/HospitForm/HospitForm2.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/HospitForm/HospitForm2.tsx new file mode 100644 index 000000000..fb49f8c15 --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/HospitForm/HospitForm2.tsx @@ -0,0 +1,249 @@ +import React from 'react' +import { Comparators, CriteriaType, GhmDataType, HospitDataType } from 'types/requestCriterias' +import withLegacyAdapter from '../CriteriaForm/legacyFormAdapter' +import { + CriteriaData, + NewDurationRangeType, + OccurenceDataType, + WithEncounterStatusDataType, + WithOccurenceCriteriaDataType +} from '../CriteriaForm/types' +import { LabelObject } from 'types/searchCriterias' +import { Link } from '@mui/material' +import { SourceType } from 'types/scope' + +type NewHospitDataType = WithOccurenceCriteriaDataType & + WithEncounterStatusDataType & { + hospitReason: string + inUteroTransfer: LabelObject[] | null + pregnancyMonitoring: LabelObject[] | null + vme: LabelObject[] | null + maturationCorticotherapie: LabelObject[] | null + chirurgicalGesture: LabelObject[] | null + childbirth: LabelObject[] | null + hospitalChildBirthPlace: LabelObject[] | null + otherHospitalChildBirthPlace: LabelObject[] | null + homeChildBirthPlace: LabelObject[] | null + childbirthMode: LabelObject[] | null + maturationReason: LabelObject[] | null + maturationModality: LabelObject[] | null + imgIndication: LabelObject[] | null + laborOrCesareanEntry: LabelObject[] | null + pathologyDuringLabor: LabelObject[] | null + obstetricalGestureDuringLabor: LabelObject[] | null + analgesieType: LabelObject[] | null + birthDeliveryDate: NewDurationRangeType | null + birthDeliveryWeeks: OccurenceDataType | null + birthDeliveryDays: OccurenceDataType | null + birthDeliveryWay: LabelObject[] | null + instrumentType: LabelObject[] | null + cSectionModality: LabelObject[] | null + presentationAtDelivery: LabelObject[] | null + birthMensurationsGrams: OccurenceDataType | null + birthMensurationsPercentil: OccurenceDataType | null + apgar1: OccurenceDataType | null + apgar3: OccurenceDataType | null + apgar5: OccurenceDataType | null + apgar10: OccurenceDataType | null + arterialPhCord: OccurenceDataType | null + arterialCordLactates: OccurenceDataType | null + birthStatus: LabelObject[] | null + postpartumHemorrhage: LabelObject[] | null + conditionPerineum: LabelObject[] | null + exitPlaceType: LabelObject[] | null + feedingType: LabelObject[] | null + complication: LabelObject[] | null + exitFeedingMode: LabelObject[] | null + exitDiagnostic: LabelObject[] | null + } + +export default withLegacyAdapter({ + form: { + label: 'GHM', + initialData: { + title: 'Critères GHM', + isInclusive: true, + occurrence: { + value: 1, + comparator: Comparators.GREATER_OR_EQUAL + }, + encounterService: null, + encounterStatus: [], + startOccurrence: null, + hospitReason: '', + inUteroTransfer: null, + pregnancyMonitoring: null, + vme: null, + maturationCorticotherapie: null, + chirurgicalGesture: null, + childbirth: null, + hospitalChildBirthPlace: null, + otherHospitalChildBirthPlace: null, + homeChildBirthPlace: null, + childbirthMode: null, + maturationReason: null, + maturationModality: null, + imgIndication: null, + laborOrCesareanEntry: null, + pathologyDuringLabor: null, + obstetricalGestureDuringLabor: null, + analgesieType: null, + birthDeliveryDate: null, + birthDeliveryWeeks: null, + birthDeliveryDays: null, + birthDeliveryWay: null, + instrumentType: null, + cSectionModality: null, + presentationAtDelivery: null, + birthMensurationsGrams: null, + birthMensurationsPercentil: null, + apgar1: null, + apgar3: null, + apgar5: null, + apgar10: null, + arterialPhCord: null, + arterialCordLactates: null, + birthStatus: null, + postpartumHemorrhage: null, + conditionPerineum: null, + exitPlaceType: null, + feedingType: null, + complication: null, + exitFeedingMode: null, + exitDiagnostic: null + }, + warningAlert: [ +
+ Données actuellement disponibles : PMSI ORBIS. Pour plus d'informations sur les prochaines intégrations de + données, veuillez vous référer au tableau trimestriel de disponibilité des données disponible{' '} + + ici + +
+ ], + errorMessages: {}, + itemSections: [ + { + items: [ + { + valueKey: 'occurrence', + type: 'occurrence', + label: "Nombre d'occurences", + withHierarchyInfo: true + }, + { + valueKey: 'code', + type: 'codeSearch', + valueSetId: 'GHM', + noOptionsText: 'Aucun GHM trouvé', + label: 'Code GHM' + }, + { + valueKey: 'encounterStatus', + type: 'autocomplete', + label: 'Statut de la visite associée', + valueSetId: 'ENCOUNTER_STATUS', + noOptionsText: 'Aucun statut trouvé' + } + ] + }, + { + title: 'Options avancées', + defaulCollapsed: true, + items: [ + { + valueKey: 'encounterService', + label: 'Unité exécutrice', + type: 'executiveUnit', + sourceType: SourceType.GHM + }, + { + valueKey: 'encounterStartDate', + type: 'duration', + errorType: 'ADVANCED_INPUTS_ERROR', + label: 'Début de prise en charge', + labelAltStyle: true, + extraLabel: 'Prise en charge', + withOptionIncludeNull: true + }, + { + valueKey: 'encounterEndDate', + type: 'duration', + label: 'Fin de prise en charge', + labelAltStyle: true, + info: 'Ne concerne pas les consultations', + errorType: 'ADVANCED_INPUTS_ERROR', + withOptionIncludeNull: true + }, + { + valueKey: 'startOccurrence', + type: 'duration', + errorType: 'ADVANCED_INPUTS_ERROR', + extraLabel: 'Date de classement en GHM' + } + ] + } + ] + }, + adapter: { + mapFromLegacyDataType: (legacyData: HospitDataType) => ({ + id: legacyData.id, + title: legacyData.title, + isInclusive: legacyData.isInclusive || true, + occurrence: { + value: legacyData.occurrence || 1, + comparator: legacyData.occurrenceComparator || Comparators.GREATER_OR_EQUAL + }, + encounterService: legacyData.encounterService || null, + encounterStatus: legacyData.encounterStatus, + encounterStartDate: + !legacyData.encounterStartDate[0] && !legacyData.encounterStartDate[1] + ? null + : { + start: legacyData.encounterStartDate[0] || null, + end: legacyData.encounterStartDate[1] || null, + includeNull: legacyData.includeEncounterStartDateNull || false + }, + encounterEndDate: + !legacyData.encounterEndDate[0] && !legacyData.encounterEndDate[1] + ? null + : { + start: legacyData.encounterEndDate[0] || null, + end: legacyData.encounterEndDate[1] || null, + includeNull: legacyData.includeEncounterEndDateNull || false + }, + startOccurrence: + !legacyData.startOccurrence[0] && !legacyData.startOccurrence[1] + ? null + : { + start: legacyData.startOccurrence[0] || null, + end: legacyData.startOccurrence[1] || null, + includeNull: false + }, + endOccurrence: legacyData.endOccurrence || [null, null], + code: legacyData.code + }), + mapToLegacyDataType: (data: CriteriaData) => ({ + id: data.id, + type: CriteriaType.CLAIM, + title: data.title, + isInclusive: data.isInclusive, + occurrence: data.occurrence.value, + startOccurrence: [data.startOccurrence?.start, data.startOccurrence?.end], + occurrenceComparator: data.occurrence.comparator, + encounterStatus: data.encounterStatus, + encounterService: data.encounterService || undefined, + encounterStartDate: [data.encounterStartDate?.start, data.encounterStartDate?.end], + encounterEndDate: [data.encounterEndDate?.start, data.encounterEndDate?.end], + includeEncounterStartDateNull: data.encounterStartDate?.includeNull, + includeEncounterEndDateNull: data.encounterEndDate?.includeNull, + label: undefined, + code: data.code + }), + valueSetIdToKey: (valueSetId: string) => undefined + } +}) diff --git a/src/components/CreationCohort/Requeteur.tsx b/src/components/CreationCohort/Requeteur.tsx index c1257e34c..a4b2bd2f4 100644 --- a/src/components/CreationCohort/Requeteur.tsx +++ b/src/components/CreationCohort/Requeteur.tsx @@ -22,6 +22,7 @@ import useStyles from './styles' import services from 'services/aphp' import { setCriteriaData } from 'state/criteria' import { AppConfig } from 'config' +import { initValueSets } from 'state/valueSets' const Requeteur = () => { const { @@ -38,6 +39,7 @@ const Requeteur = () => { allowSearchIpp = false } = useAppSelector((state) => state.cohortCreation.request || {}) const criteriaData = useAppSelector((state) => state.cohortCreation.criteria || {}) + const valueSets = useAppSelector((state) => state.valueSets) const config = useContext(AppConfig) const params = useParams<{ requestId: string @@ -182,7 +184,16 @@ const Requeteur = () => { return true } - // Initial useEffect + // Initial useEffects + + useEffect(() => { + // TODO maybe put this in the other useEffect or elsewhere to be sure the valuesets are loaded before displaying the criterias + ;(async () => { + if (valueSets.loading || valueSets.loaded) { + await dispatch(initValueSets(criteriaList())).unwrap() + } + })() + }, [dispatch, valueSets]) useEffect(() => { _fetchRequest() diff --git a/src/components/Filters/DatesRangeFilter/index.tsx b/src/components/Filters/DatesRangeFilter/index.tsx index 5038500ce..634fe213a 100644 --- a/src/components/Filters/DatesRangeFilter/index.tsx +++ b/src/components/Filters/DatesRangeFilter/index.tsx @@ -1,4 +1,5 @@ import CalendarRange from 'components/ui/Inputs/CalendarRange' +import { BlockWrapper } from 'components/ui/Layout' import { FormContext } from 'components/ui/Modal' import React, { useContext, useEffect, useState } from 'react' import { DurationRangeType } from 'types/searchCriterias' @@ -27,16 +28,18 @@ const DatesRangeFilter = ({ names, values, disabled }: DatesRangeFilterProps) => }, [endDate]) return ( - { - setStartDate(value[0]) - setEndDate(value[1]) - }} - onError={onError} - /> + + { + setStartDate(value[0]) + setEndDate(value[1]) + }} + onError={onError} + /> + ) } diff --git a/src/components/Filters/DocStatusFilter/index.tsx b/src/components/Filters/DocStatusFilter/index.tsx index 40de56190..2786b2f72 100644 --- a/src/components/Filters/DocStatusFilter/index.tsx +++ b/src/components/Filters/DocStatusFilter/index.tsx @@ -16,7 +16,7 @@ const DocStatusFilter = ({ name, value, docStatusesList, disabled = false }: Doc useEffect(() => { if (context?.updateFormData) context.updateFormData(name, docStatuses) - }, [docStatuses]) + }, [docStatuses, context, name]) return ( diff --git a/src/components/ui/CriteriaLayout/index.tsx b/src/components/ui/CriteriaLayout/index.tsx index dedd59fc0..9d6915b48 100644 --- a/src/components/ui/CriteriaLayout/index.tsx +++ b/src/components/ui/CriteriaLayout/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactNode } from 'react' import useStyles from './styles' import { Alert, Button, Divider, FormLabel, Grid, IconButton, Switch, TextField, Typography } from '@mui/material' import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' @@ -14,9 +14,9 @@ type CriteriaLayoutProps = { onChangeTitle: (title: string) => void isInclusive: boolean onChangeIsInclusive: (isInclusive: boolean) => void - infoAlert?: string[] - warningAlert?: string[] - errorAlert?: string[] + infoAlert?: ReactNode[] + warningAlert?: ReactNode[] + errorAlert?: ReactNode[] } const CriteriaLayout: React.FC> = ({ diff --git a/src/components/ui/Inputs/CalendarRange/index.tsx b/src/components/ui/Inputs/CalendarRange/index.tsx index cad7b4f5d..30bfe5abe 100644 --- a/src/components/ui/Inputs/CalendarRange/index.tsx +++ b/src/components/ui/Inputs/CalendarRange/index.tsx @@ -15,7 +15,7 @@ interface CalendarRangeProps { label?: ReactNode inline?: boolean disabled?: boolean - onChange: (newDuration: DurationRangeType) => void + onChange: (newDuration: DurationRangeType, includeNullValues: boolean) => void onError: (isError: boolean) => void includeNullValues?: boolean onChangeIncludeNullValues?: (includeNullValues: boolean) => void @@ -44,7 +44,7 @@ const CalendarRange = ({ setError({ isError: true, errorMessage: 'La date maximale doit être supérieure à la date minimale.' }) onError(true) } else { - onChange([startDate, endDate]) + onChange([startDate, endDate], isNullValuesChecked) if (onChangeIncludeNullValues) { onChangeIncludeNullValues(isNullValuesChecked) } @@ -52,7 +52,7 @@ const CalendarRange = ({ }, [startDate, endDate, isNullValuesChecked]) return ( - + <> {isString(label) ? ( {label} : @@ -97,7 +97,7 @@ const CalendarRange = ({ {error.errorMessage} )} - + ) } diff --git a/src/state/store.ts b/src/state/store.ts index 97d1e40d2..200b9e52c 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -23,6 +23,7 @@ import scope from './scope' import pmsi from './pmsi' import me from './me' import syncHierarchyTable from './syncHierarchyTable' +import valueSets from './valueSets' const cohortCreationReducer = combineReducers({ criteria, @@ -32,6 +33,7 @@ const cohortCreationReducer = combineReducers({ const rootReducer = combineReducers({ me, cohortCreation: cohortCreationReducer, + valueSets, exploredCohort, drawer, message, diff --git a/src/state/valueSets.ts b/src/state/valueSets.ts new file mode 100644 index 000000000..0ec1d94fc --- /dev/null +++ b/src/state/valueSets.ts @@ -0,0 +1,93 @@ +import { createSlice, createEntityAdapter, createAsyncThunk } from '@reduxjs/toolkit' +import { fetchValueSet } from 'services/aphp/callApi' +import { CriteriaItemType, HierarchyElementWithSystem } from 'types' + +type ValueSetOptions = { + id: string + options: HierarchyElementWithSystem[] +} + +const getAllCriteriaItems = (criteria: CriteriaItemType[]): CriteriaItemType[] => { + const allCriteriaItems: CriteriaItemType[] = [] + for (const criterion of criteria) { + allCriteriaItems.push(criterion) + if (criterion.subItems && criterion.subItems.length > 0) { + allCriteriaItems.push(...getAllCriteriaItems(criterion.subItems)) + } + } + return allCriteriaItems +} + +export const prefetchSmallValueSets = async (criteriaTree: CriteriaItemType[]): Promise> => { + const criteriaList = getAllCriteriaItems(criteriaTree) + + // fetch all unique valueSetIds from the criteriaList + const uniqueValueSetIds = criteriaList + .flatMap((criterion) => { + const criterionValuesets = criterion.formDefinition?.itemSections.flatMap((section) => { + const sectionValuesets = section.items + .map((item) => { + if (item.type === 'autocomplete') { + return item.valueSetId + } + return null + }) + .filter((item) => item !== null) + .map((item) => item as string) + return sectionValuesets + }) + + return criterionValuesets || [] + }) + .reduce((acc, item) => { + if (!acc.some((existingItem) => existingItem === item)) { + acc.push(item) + } + return acc + }, [] as string[]) + + // fetch them + return await Promise.all( + uniqueValueSetIds.map(async (valueSetUrl) => { + const options = await fetchValueSet(valueSetUrl, { + joinDisplayWithCode: false, + sortingKey: 'id' + }) + return { id: valueSetUrl, options } + }) + ) +} + +export const initValueSets = createAsyncThunk('valueSets/initValueSets', async (criteriaList: CriteriaItemType[]) => { + const response = await prefetchSmallValueSets(criteriaList) + return response +}) + +const valueSetsAdapter = createEntityAdapter() + +const valueSetsSlice = createSlice({ + name: 'valueSets', + initialState: valueSetsAdapter.getInitialState({ loading: false, error: false, loaded: false }), + reducers: { + addValueSets: valueSetsAdapter.addMany + }, + extraReducers: (builder) => { + builder + .addCase(initValueSets.pending, (state) => { + state.loading = true + state.error = false + }) + .addCase(initValueSets.fulfilled, (state, action) => { + valueSetsAdapter.setAll(state, action.payload) + state.loading = false + state.loaded = true + }) + .addCase(initValueSets.rejected, (state) => { + state.loading = false + state.error = true + }) + } +}) + +export const { addValueSets } = valueSetsSlice.actions +export default valueSetsSlice.reducer diff --git a/src/types.ts b/src/types.ts index 05a8fdf84..c1c541700 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,7 @@ import { Comparators, CriteriaDataKey, CriteriaType, ResourceType, SelectedCrite import { ExportTableType } from 'components/Dashboard/ExportModal/export_table' import { Hierarchy } from 'types/hierarchy' import { SearchByTypes } from 'types/searchCriterias' +import { CriteriaForm } from 'components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/types' export enum JobStatus { new = 'new', @@ -360,6 +361,9 @@ export type CriteriaItemDataCache = { criteriaType: string // eslint-disable-next-line @typescript-eslint/no-explicit-any data: { [key in CriteriaDataKey]?: any } + smallValuesets?: { + [key: string]: HierarchyElementWithSystem[] + } } export type CriteriaItemType = { @@ -371,6 +375,7 @@ export type CriteriaItemType = { disabled?: boolean fetch?: { [key in CriteriaDataKey]?: FetchFunctionVariant } subItems?: CriteriaItemType[] + formDefinition?: CriteriaForm } type FetchFunctionVariant = @@ -714,7 +719,7 @@ export type HierarchyTree = null | { loading?: number } -export type HierarchyElementWithSystem = Hierarchy & { system?: string } +export type HierarchyElementWithSystem = Hierarchy & { system?: string } export type ScopeElement = { id: string diff --git a/src/utils/cohortCreation.ts b/src/utils/cohortCreation.ts index 2c5c29d4d..c83a1839c 100644 --- a/src/utils/cohortCreation.ts +++ b/src/utils/cohortCreation.ts @@ -7,7 +7,8 @@ import { CriteriaItemDataCache, BiologyStatus, CriteriaGroupType, - ScopeElement + ScopeElement, + HierarchyElementWithSystem } from 'types' import { @@ -89,6 +90,7 @@ import { QuestionnaireResponseParamsKeys } from 'mappers/filters' import { getConfig } from 'config' +import { fetchValueSet } from 'services/aphp/callApi' const REQUETEUR_VERSION = 'v1.4.5'