Skip to content

Commit

Permalink
feat: add conditional biology value criteria item
Browse files Browse the repository at this point in the history
  • Loading branch information
pl-buiquang committed Dec 3, 2024
1 parent 9268ccd commit bc5834e
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,21 @@ export const renderLabel = (label: string, tooltip?: string, altStyle?: boolean)
}

export const CFItem = <T, V extends DataTypeMappings, U extends CriteriaItem<T, V>>(props: CritieraItemProps<T, V>) => {
const { valueKey, updateData, data, setError, getValueSetOptions, searchCode, viewRenderers } = props
const { valueKey, updateData, data, setError, getValueSetOptions, searchCode, viewRenderers, resetCondition } = props
const View = viewRenderers[props.type] as CriteriaFormItemView<U['type']>
const fieldValue = (valueKey ? data[valueKey] : undefined) as DataTypeMapping[U['type']]['dataType']
const fieldValue = (valueKey ? data[valueKey] : undefined) as DataTypeMapping[U['type']]['dataType'] | null
const context = { deidentified: props.deidentified }
const displayCondition = props.displayCondition
console.log('rerendering', props.label, data)

if (
valueKey &&
fieldValue !== null &&
resetCondition &&
((isFunction(resetCondition) && resetCondition(data as Record<string, DataTypes>, context)) ||
(isString(resetCondition) && eval(resetCondition)(data, context)))
) {
updateData({ ...data, [valueKey]: null })
}
if (
displayCondition &&
((isFunction(displayCondition) && !displayCondition(data as Record<string, DataTypes>, context)) ||
Expand All @@ -79,7 +88,7 @@ export const CFItem = <T, V extends DataTypeMappings, U extends CriteriaItem<T,
context={context}
>
<View
value={fieldValue}
value={fieldValue as DataTypeMapping[U['type']]['dataType']}
disabled={disabled}
definition={props}
updateData={(value) => valueKey && updateData({ ...data, [valueKey]: value })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,14 @@ export const UNBUILD_MAPPERS = {
args: Array<DataTypes>
) => {
Promise.resolve((args[0] as LabelObject[]).find((l) => l.id === fhirkey))
}
},
unbuildBooleanFromDataNonNullStatus: async (
val: string,
deid: boolean,
existingValue: DataTypes,
fhirkey: string,
args: Array<DataTypes>
) => Promise.resolve(!!val)
}

export const BUILD_MAPPERS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const getLabelsForCodeSearchItem = (
.map((value) => {
return (
(value.system ? valueSets.cache[value.system] : item.valueSetIds.flatMap((vid) => valueSets.cache[vid])) || []
).find((code) => code.id === value.id) as LabelObject
).find((code) => code && code.id === value.id) as LabelObject
})
.filter((code) => code !== undefined)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import extractFilterParams, { FhirFilter } from 'utils/fhirFilterParser'
import { CriteriaItemType } from 'types'
import { ValueSetStore } from 'state/valueSets'
import { ReactNode } from 'react'
import { isArray, isString } from 'lodash'
import { isArray, isFunction, isString } from 'lodash'
import { BUILD_MAPPERS, BuilderMethod, UNBUILD_MAPPERS } from './buildMappers'

/************************************************************************************/
Expand Down Expand Up @@ -199,7 +199,15 @@ export const constructFhirFilterForType = <T extends SelectedCriteriaType>(
? parseFhirKey(buildInfo.fhirKey, deidentified, criteria)
: buildInfo.fhirKey
const buildMethod = buildInfo.buildMethod ? BUILD_MAPPERS[buildInfo.buildMethod] : DEFAULT_BUILD_METHOD[item.type]
const value = criteria[item.valueKey] as DataTypes
let value = criteria[item.valueKey] as DataTypes
if (buildInfo.ignoreIf) {
const ignore = isFunction(buildInfo.ignoreIf)
? buildInfo.ignoreIf(criteria)
: eval(buildInfo.ignoreIf)(criteria)
if (ignore) {
value = null
}
}
const filterValues = buildMethod(
value,
fhirKey,
Expand Down Expand Up @@ -267,21 +275,31 @@ export const unbuildCriteriaDataFromDefinition = async <T extends SelectedCriter
const key = filter[0] ?? null
const value = filter[1] ?? null
if (key !== null) {
const item = criteriaItems.find((item) => matchFhirKey(key, item.buildInfo?.fhirKey))
if (item?.buildInfo?.fhirKey && item?.valueKey) {
const unbuildMethod = item.buildInfo.unbuildMethod
? UNBUILD_MAPPERS[item.buildInfo.unbuildMethod]
: DEFAULT_UNBUILD_METHOD[item.type]
const dataValue = await unbuildMethod(
value,
!isString(item.buildInfo.fhirKey) &&
'deid' in item.buildInfo.fhirKey &&
item.buildInfo?.fhirKey.deid === key,
emptyCriterion[item?.valueKey] as DataTypes,
key,
item.buildInfo.unbuildMethodExtraArgs || []
)
emptyCriterion[item?.valueKey] = dataValue as T[keyof T]
const matchingItems = criteriaItems.filter((item) => matchFhirKey(key, item.buildInfo?.fhirKey))
for (const item of matchingItems) {
if (item?.buildInfo?.fhirKey && item?.valueKey) {
if (
item.buildInfo.unbuildIgnoreValues &&
item.buildInfo.unbuildIgnoreValues.find(
(ignoreValue) => JSON.stringify(ignoreValue) === JSON.stringify(value)
)
) {
continue
}
const unbuildMethod = item.buildInfo.unbuildMethod
? UNBUILD_MAPPERS[item.buildInfo.unbuildMethod]
: DEFAULT_UNBUILD_METHOD[item.type]
const dataValue = await unbuildMethod(
value,
!isString(item.buildInfo.fhirKey) &&
'deid' in item.buildInfo.fhirKey &&
item.buildInfo?.fhirKey.deid === key,
emptyCriterion[item.valueKey] as DataTypes,
key,
item.buildInfo.unbuildMethodExtraArgs || []
)
emptyCriterion[item.valueKey] = dataValue as T[keyof T]
}
}
}
}
Expand Down Expand Up @@ -309,7 +327,15 @@ export const criteriasAsArray = (
.flatMap((section) => section.items)
.map((item) => {
if (!item.buildInfo || !item.valueKey) return null
const val = selectedCriteria[item.valueKey as keyof typeof selectedCriteria]
let val = selectedCriteria[item.valueKey as keyof typeof selectedCriteria]
if (item.buildInfo.ignoreIf) {
const ignore = isFunction(item.buildInfo.ignoreIf)
? item.buildInfo.ignoreIf(selectedCriteria)
: eval(item.buildInfo.ignoreIf)(selectedCriteria)
if (ignore) {
val = null
}
}
if (!val || (isArray(val) && val.length === 0)) return null
const chipBuilder = item.buildInfo?.chipDisplayMethod
? CHIPS_DISPLAY_METHODS[item.buildInfo?.chipDisplayMethod]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Autocomplete,
Checkbox,
FormControlLabel,
FormLabel,
Grid,
Radio,
RadioGroup,
Expand All @@ -27,6 +26,8 @@ import { BlockWrapper } from 'components/ui/Layout'
import { fetchValueSet } from 'services/aphp/callApi'
import DurationRange from 'components/ui/Inputs/DurationRange'
import { IndeterminateCheckBoxOutlined } from '@mui/icons-material'
import { CriteriaLabel } from 'components/ui/CriteriaLabel'
import { Comparators } from 'types/requestCriterias'

/************************************************************************************/
/* Criteria Form Item Renderer */
Expand Down Expand Up @@ -203,7 +204,7 @@ const FORM_ITEM_RENDERER: { [key in CriteriaFormItemType]: CriteriaFormItemView<
)
},
numberAndComparator: (props) => {
const nonNullValue = props.value ?? { value: 0, comparator: 'GREATER_OR_EQUAL' }
const nonNullValue = props.value ?? { value: 0, comparator: Comparators.GREATER_OR_EQUAL }
return (
<OccurenceInput
floatValues={props.definition.floatValues}
Expand All @@ -223,11 +224,19 @@ const FORM_ITEM_RENDERER: { [key in CriteriaFormItemType]: CriteriaFormItemView<
},
boolean: (props) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { classes } = useStyles()
return (
<BlockWrapper className={classes.inputItem} container alignItems="center">
<FormLabel component="legend">{props.definition.label}</FormLabel>
<Switch checked={props.value} onChange={(event) => props.updateData(event.target.checked)} color="secondary" />
<BlockWrapper container alignItems="center">
<CriteriaLabel
label={props.definition.label || ''}
infoIcon={props.definition.extraInfo}
style={{ padding: 0 }}
>
<Switch
checked={props.value}
onChange={(event) => props.updateData(event.target.checked)}
disabled={props.disabled}
/>
</CriteriaLabel>
</BlockWrapper>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ type BaseCriteriaItem = {
extraLabel?: string | ((data: Record<string, DataTypes>, context: Context) => string)
extraInfo?: string
// for conditionnal fields
displayCondition?: ((data: Record<string, DataTypes>, context: Context) => boolean) | string
disableCondition?: ((data: Record<string, DataTypes>, context: Context) => boolean) | string
displayValueSummary?: (data: DataTypes) => string | string
displayCondition?: ((data: Record<string, DataTypes>, context: Context) => boolean) | string // the displayCondition is used to hide the field
disableCondition?: ((data: Record<string, DataTypes>, context: Context) => boolean) | string // the disableCondition is used to disable the field
displayValueSummary?: (data: DataTypes) => string | string // the displayValueSummary is used to display a summary of the value
// for resetting the value of the field
resetCondition?: ((data: Record<string, DataTypes>, context: Context) => boolean) | string // the resetCondition is used to reset the value of the field
}

type WithLabel = {
Expand Down Expand Up @@ -77,14 +79,13 @@ export type RadioChoiceCriteriaItem = BaseCriteriaItem & {
choices: LabelObject[]
}

export type NumberWithComparatorCriteriaItem = BaseCriteriaItem &
WithLabel & {
type: 'numberAndComparator'
withHierarchyInfo?: boolean
withInfo?: ReactNode
floatValues?: boolean
allowBetween?: boolean
}
export type NumberWithComparatorCriteriaItem = BaseCriteriaItem & {
type: 'numberAndComparator'
withHierarchyInfo?: boolean
withInfo?: ReactNode
floatValues?: boolean
allowBetween?: boolean
}

export type DurationItem = BaseCriteriaItem & {
type: 'durationRange'
Expand Down Expand Up @@ -277,14 +278,15 @@ export type FhirKey =

export type CriteriaItemBuildInfo = {
buildInfo?: {
fhirKey?: FhirKey
buildMethod?: keyof typeof BUILD_MAPPERS
buildMethodExtraArgs?: Array<BuildMethodExtraParam>
ignoreIf?: ((data: Record<string, DataTypes>, context: Context) => boolean) | string
unbuildMethod?: keyof typeof UNBUILD_MAPPERS
unbuildMethodExtraArgs?: Array<DataTypes>
chipDisplayMethod?: keyof typeof CHIPS_DISPLAY_METHODS
chipDisplayMethodExtraArgs?: Array<BuildMethodExtraParam>
fhirKey?: FhirKey // the key (fhir param name) to use in the fhir filter
buildMethod?: keyof typeof BUILD_MAPPERS // one of the build mappers, if not provided the default build method for this type of criteria will be used
buildMethodExtraArgs?: Array<BuildMethodExtraParam> // extra arguments to pass to the build method
ignoreIf?: ((data: Record<string, DataTypes>) => boolean) | string // if true, the criteria value will be set to null and therefore ignored in the build / chip display process
unbuildMethod?: keyof typeof UNBUILD_MAPPERS // one of the unbuild mappers, if not provided the default unbuild method for this type of criteria will be used
unbuildMethodExtraArgs?: Array<DataTypes> // extra arguments to pass to the unbuild method
unbuildIgnoreValues?: DataTypes[] // if the filter raw value is found in this list, unbuilding (setting the value to the criteria data object from the filter) will be ignored for this criteria. It is mainly used to match default values
chipDisplayMethod?: keyof typeof CHIPS_DISPLAY_METHODS // one of the chip display mappers, if not provided the default chip display method for this type of criteria will be used
chipDisplayMethodExtraArgs?: Array<BuildMethodExtraParam> // extra arguments to pass to the chip display method
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ObservationDataType = CommonCriteriaData &
type: CriteriaType.OBSERVATION
code: LabelObject[] | null
searchByValue: NumberAndComparatorDataType | null
enableSearchByValue: boolean
}

export const form: () => CriteriaForm<ObservationDataType> = () => ({
Expand All @@ -37,7 +38,8 @@ export const form: () => CriteriaForm<ObservationDataType> = () => ({
encounterEndDate: null,
encounterStatus: [],
code: null,
searchByValue: null
searchByValue: null,
enableSearchByValue: false
},
infoAlert: ['Tous les éléments des champs multiples sont liés par une contrainte OU'],
warningAlert: [
Expand Down Expand Up @@ -85,31 +87,53 @@ export const form: () => CriteriaForm<ObservationDataType> = () => ({
}
},
{
// TODO: ajouter une checkbox (??? l'UX était pas top en vrai) pour valider l'ajout d'une recherche par valeur
valueKey: 'enableSearchByValue',
label: 'Activer la recherche par valeur',
info: 'Pour pouvoir rechercher par valeur, vous devez sélectionner un seul et unique analyte (élement le plus fin de la hiérarchie).',
type: 'boolean',
displayCondition: (data) => {
const typedData = data as ObservationDataType
return typedData.code?.length === 1 && !!typedData.code[0].isLeaf
},
resetCondition: (data) => {
const typedData = data as ObservationDataType
return typedData.code?.length !== 1 || !typedData.code[0].isLeaf
},
buildInfo: {
fhirKey: ObservationParamsKeys.VALUE,
buildMethod: 'noop',
chipDisplayMethod: 'noop',
unbuildMethod: 'unbuildBooleanFromDataNonNullStatus'
}
},
{
valueKey: 'searchByValue',
type: 'numberAndComparator',
label: 'Recherche par valeur',
info: 'Pour pouvoir rechercher par valeur, vous devez sélectionner un seul et unique analyte (élement le plus fin de la hiérarchie).',
allowBetween: true,
withHierarchyInfo: false,
disableCondition: (data) => {
displayCondition: (data) => {
return !!data.enableSearchByValue
},
resetCondition: (data) => {
const typedData = data as ObservationDataType
return typedData.code?.length !== 1 || !typedData.code[0].isLeaf
},
buildInfo: {
fhirKey: ObservationParamsKeys.VALUE,
ignoreIf: (data) => {
const typedData = data as ObservationDataType
return typedData.code?.length !== 1 || !typedData.code[0].isLeaf
return typedData.code?.length !== 1 || !typedData.code[0].isLeaf || !data.enableSearchByValue
},
buildMethodExtraArgs: [{ type: 'string', value: 'le0,ge0' }],
chipDisplayMethodExtraArgs: [{ type: 'string', value: 'Valeur' }]
chipDisplayMethodExtraArgs: [{ type: 'string', value: 'Valeur' }],
unbuildIgnoreValues: ['le0,ge0']
}
},
{
valueKey: 'encounterStatus',
type: 'autocomplete',
label: 'Statut de la visite associée',
extraLabel: () => 'Statut de la visite',
valueSetId: getConfig().core.valueSets.encounterStatus.url,
noOptionsText: 'Veuillez entrer un statut de visite associée',
buildInfo: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,7 @@ export const form: () => CriteriaForm<MedicationDataType> = () => ({
displayCondition: (data) => data.type === CriteriaType.MEDICATION_REQUEST,
buildInfo: {
fhirKey: PrescriptionParamsKeys.PRESCRIPTION_TYPES,
buildMethodExtraArgs: [
{ type: 'method', value: 'buildLabelObject' },
{ type: 'reference', value: 'type' },
{ type: 'string', value: ResourceType.MEDICATION_ADMINISTRATION }
],
buildMethod: 'skipIf'
ignoreIf: (data) => data.type === CriteriaType.MEDICATION_ADMINISTRATION
}
},
{
Expand Down
6 changes: 4 additions & 2 deletions src/components/ui/CriteriaLabel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react'
import React, { PropsWithChildren } from 'react'
import { StyledFormLabel } from './styles'
import { FormLabelProps, Tooltip } from '@mui/material'
import InfoIcon from '@mui/icons-material/Info'

type StyledFormLabelProps = FormLabelProps & {
label: string
infoIcon?: React.ReactNode
style?: React.CSSProperties
}

export const CriteriaLabel = (props: StyledFormLabelProps) => {
export const CriteriaLabel = (props: PropsWithChildren<StyledFormLabelProps>) => {
const { infoIcon, label } = props
return (
<StyledFormLabel component="legend" {...props}>
Expand All @@ -18,6 +19,7 @@ export const CriteriaLabel = (props: StyledFormLabelProps) => {
<InfoIcon fontSize="small" color="primary" style={{ marginLeft: 4 }} />
</Tooltip>
)}
{props.children}
</StyledFormLabel>
)
}
1 change: 1 addition & 0 deletions src/components/ui/Inputs/Occurences/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const OccurenceInput = ({
}
return null
}
console.log('comparatorValue', comparatorValue)

return (
<>
Expand Down

0 comments on commit bc5834e

Please sign in to comment.