From 5bb121d929619f508147b72d86ce1217ee3f9e20 Mon Sep 17 00:00:00 2001 From: Mehdi-BOUYAHIA <63298037+Mehdi-BOUYAHIA@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:57:39 +0100 Subject: [PATCH] fix: fix deidentied use case for date range into patient and encounter * release: release version 2.29.2 * fix: fix deidentied use case for date range into patient and encounter --- .../DemographicForm/index.tsx | 37 +++++--- .../EncounterForm/index.tsx | 10 +++ src/utils/age.ts | 10 +-- src/utils/cohortCreation.ts | 86 ++++++++++++------- 4 files changed, 98 insertions(+), 45 deletions(-) diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx index 4ed752010..c95639332 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx @@ -17,6 +17,8 @@ import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' import useStyles from './styles' +import { useAppSelector } from 'state' + import { DurationRangeType, LabelObject, VitalStatusLabel } from 'types/searchCriterias' import CalendarRange from 'components/ui/Inputs/CalendarRange' import DurationRange from 'components/ui/Inputs/DurationRange' @@ -53,6 +55,15 @@ const DemographicForm = (props: CriteriaDrawerComponentProps) => { const [title, setTitle] = useState(selectedCriteria?.title || 'Critère démographique') const [isInclusive, setIsInclusive] = useState(selectedCriteria?.isInclusive || true) + const selectedPopulation = useAppSelector((state) => state.cohortCreation.request.selectedPopulation || []) + + const deidentified: boolean | undefined = + selectedPopulation === null + ? undefined + : selectedPopulation + .map((population) => population && population.access) + .filter((elem) => elem && elem === 'Pseudonymisé').length > 0 + const { classes } = useStyles() const [error, setError] = useState(Error.NO_ERROR) @@ -153,16 +164,18 @@ const DemographicForm = (props: CriteriaDrawerComponentProps) => { renderInput={(params) => } /> - - setBirthdates(value)} - onError={(isError) => setError(isError ? Error.INCOHERENT_AGE_ERROR : Error.NO_ERROR)} - /> - + {!deidentified && ( + + setBirthdates(value)} + onError={(isError) => setError(isError ? Error.INCOHERENT_AGE_ERROR : Error.NO_ERROR)} + /> + + )} { } onChange={(value) => setAge(value)} onError={(isError) => setError(isError ? Error.INCOHERENT_AGE_ERROR : Error.NO_ERROR)} + deidentified={deidentified} /> - {vitalStatus && + {!deidentified && + vitalStatus && (vitalStatus.length === 0 || (vitalStatus.length === 1 && vitalStatus.find((status: LabelObject) => status.label === VitalStatusLabel.DECEASED))) && ( diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/EncounterForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/EncounterForm/index.tsx index 20b9ea172..07dea5b4a 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/EncounterForm/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/EncounterForm/index.tsx @@ -17,6 +17,7 @@ import { import InfoIcon from '@mui/icons-material/Info' import useStyles from './styles' +import { useAppSelector } from 'state' import { CriteriaDrawerComponentProps, CriteriaName, ScopeTreeRow } from 'types' import PopulationCard from '../../../../PopulationCard/PopulationCard' @@ -99,6 +100,14 @@ const EncounterForm = ({ const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) const isEdition = selectedCriteria !== null ? true : false const [error, setError] = useState(Error.NO_ERROR) + const selectedPopulation = useAppSelector((state) => state.cohortCreation.request.selectedPopulation || []) + + const deidentified: boolean | undefined = + selectedPopulation === null + ? undefined + : selectedPopulation + .map((population) => population && population.access) + .filter((elem) => elem && elem === 'Pseudonymisé').length > 0 useEffect(() => { setError(Error.NO_ERROR) @@ -246,6 +255,7 @@ const EncounterForm = ({ value={age} onChange={(value) => setAge(value)} onError={(isError) => setError(isError ? Error.INCOHERENT_AGE_ERROR : Error.NO_ERROR)} + deidentified={deidentified} /> diff --git a/src/utils/age.ts b/src/utils/age.ts index 7005adbf3..258d1eb47 100644 --- a/src/utils/age.ts +++ b/src/utils/age.ts @@ -125,13 +125,13 @@ export const convertDurationToTimestamp = (duration: DurationType | null): numbe return year * 365 + month * 30 + day } -export const convertTimestampToDuration = (timestamp: number | null): DurationType => { +export const convertTimestampToDuration = (timestamp: number | null, type: Calendar): DurationType => { const duration: DurationType = { year: 130, month: 0, day: 0 } if (!timestamp) return duration - duration.year = Math.floor(timestamp / 365) - timestamp = timestamp % 365 - duration.month = Math.floor(timestamp / 30) - timestamp = timestamp % 30 + duration.year = type === Calendar.MONTH ? Math.floor(timestamp / 12) : Math.floor(timestamp / 365) + timestamp = type === Calendar.MONTH ? timestamp % 12 : timestamp % 365 + duration.month = type === Calendar.MONTH ? Math.floor(timestamp / 1) : Math.floor(timestamp / 30) + timestamp = type === Calendar.MONTH ? timestamp % 1 : timestamp % 30 duration.day = timestamp return duration } diff --git a/src/utils/cohortCreation.ts b/src/utils/cohortCreation.ts index 6de1aae3d..9b1c8ae19 100644 --- a/src/utils/cohortCreation.ts +++ b/src/utils/cohortCreation.ts @@ -24,7 +24,8 @@ import { convertDurationToString, convertDurationToTimestamp, convertStringToDuration, - convertTimestampToDuration + convertTimestampToDuration, + substructAgeString } from './age' import { Comparators, DocType, RessourceType, SelectedCriteriaType, CriteriaDataKey } from 'types/requestCriterias' import { comparatorToFilter, parseOccurence } from './valueComparator' @@ -36,7 +37,8 @@ const IPP_LIST_FHIR = 'identifier.value' const PATIENT_GENDER = 'gender' const PATIENT_BIRTHDATE = 'birthdate' -const PATIENT_AGE = 'age-day' +const PATIENT_AGE_DAY = 'age-day' +const PATIENT_AGE_MONTH = 'age-month' const PATIENT_DEATHDATE = 'death-date' const PATIENT_DECEASED = 'deceased' @@ -191,7 +193,7 @@ export const getCalendarMultiplicator = (type: Calendar): number => { } } -const constructFilterFhir = (criterion: SelectedCriteriaType): string => { +const constructFilterFhir = (criterion: SelectedCriteriaType, deidentified: boolean): string => { let filterFhir = '' const filterReducer = (accumulator: any, currentValue: any): string => accumulator ? `${accumulator}&${currentValue}` : currentValue ? currentValue : accumulator @@ -200,15 +202,16 @@ const constructFilterFhir = (criterion: SelectedCriteriaType): string => { switch (criterion.type) { case RessourceType.PATIENT: { - const ageMin = convertDurationToTimestamp( - convertStringToDuration(criterion.age?.[0]) || { year: 0, month: 0, day: 0 } - ) - const ageMax = convertDurationToTimestamp( - convertStringToDuration(criterion.age?.[1]) || { year: 130, month: 0, day: 0 } - ) + const birthdates: [string, string] = [ + moment(substructAgeString(criterion?.age?.[0] || '0/0/0')).format('MM/DD/YYYY'), + moment(substructAgeString(criterion?.age?.[1] || '0/0/130')).format('MM/DD/YYYY') + ] - const ageMinCriterion = `${PATIENT_AGE}=ge${ageMin}` - const ageMaxCriterion = `${PATIENT_AGE}=le${ageMax}` + const ageMin = Math.abs(moment(birthdates[0]).diff(moment(), deidentified ? 'months' : 'days')) + const ageMax = Math.abs(moment(birthdates[1]).diff(moment(), deidentified ? 'months' : 'days')) + + const ageMinCriterion = `${deidentified ? PATIENT_AGE_MONTH : PATIENT_AGE_DAY}=ge${ageMin}` + const ageMaxCriterion = `${deidentified ? PATIENT_AGE_MONTH : PATIENT_AGE_DAY}=le${ageMax}` filterFhir = [ 'active=true', @@ -243,6 +246,19 @@ const constructFilterFhir = (criterion: SelectedCriteriaType): string => { } case RessourceType.ENCOUNTER: { + const birthdates: [string, string] = [ + moment(substructAgeString(criterion?.age?.[0] || '0/0/0')).format('MM/DD/YYYY'), + moment(substructAgeString(criterion?.age?.[1] || '0/0/130')).format('MM/DD/YYYY') + ] + + deidentified = false //TODO erase this line when deidentified param for encounter is implemented + + const ageMin = Math.abs(moment(birthdates[0]).diff(moment(), deidentified ? 'months' : 'days')) + const ageMax = Math.abs(moment(birthdates[1]).diff(moment(), deidentified ? 'months' : 'days')) + + const ageMinCriterion = `${deidentified ? ENCOUNTER_MIN_BIRTHDATE : ENCOUNTER_MIN_BIRTHDATE}=ge${ageMin}` + const ageMaxCriterion = `${deidentified ? ENCOUNTER_MAX_BIRTHDATE : ENCOUNTER_MAX_BIRTHDATE}=le${ageMax}` + filterFhir = [ 'subject.active=true', `${ @@ -320,16 +336,8 @@ const constructFilterFhir = (criterion: SelectedCriteriaType): string => { ? `${ENCOUNTER_DURATION}=le${convertDurationToTimestamp(convertStringToDuration(criterion.duration?.[1]))}` : '' }`, - `${ - criterion.age?.[0] - ? `${ENCOUNTER_MIN_BIRTHDATE}=ge${convertDurationToTimestamp(convertStringToDuration(criterion.age?.[0]))}` - : '' - }`, - `${ - criterion.age?.[1] - ? `${ENCOUNTER_MAX_BIRTHDATE}=le${convertDurationToTimestamp(convertStringToDuration(criterion.age?.[1]))}` - : '' - }` + `${criterion.age?.[0] ? ageMinCriterion : ''}`, + `${criterion.age?.[1] ? ageMaxCriterion : ''}` ] .filter((elem) => elem) .reduce(filterReducer) @@ -661,6 +669,12 @@ export function buildRequest( ): string { if (!selectedPopulation) return '' selectedPopulation = selectedPopulation.filter((elem) => elem !== undefined) + const deidentified: boolean = + selectedPopulation === null + ? false + : selectedPopulation + .map((population) => population && population.access) + .filter((elem) => elem && elem === 'Pseudonymisé').length > 0 const exploreCriteriaGroup = (itemIds: number[]): (RequeteurCriteriaType | RequeteurGroupType)[] => { let children: (RequeteurCriteriaType | RequeteurGroupType)[] = [] @@ -677,7 +691,7 @@ export function buildRequest( _id: item.id ?? 0, isInclusive: item.isInclusive ?? true, resourceType: item.type ?? RessourceType.PATIENT, - filterFhir: constructFilterFhir(item), + filterFhir: constructFilterFhir(item, deidentified), occurrence: !(item.type === RessourceType.PATIENT || item.type === RessourceType.IPP_LIST) && item.occurrence ? { @@ -858,13 +872,23 @@ export async function unbuildRequest(_json: string): Promise { const value = filter ? filter[1] : null switch (key) { - case PATIENT_AGE: { + case PATIENT_AGE_DAY: { + if (value?.includes('ge')) { + const ageMin = value?.replace('ge', '') + currentCriterion.age[0] = convertDurationToString(convertTimestampToDuration(+ageMin, Calendar.DAY)) + } else if (value?.includes('le')) { + const ageMax = value?.replace('le', '') + currentCriterion.age[1] = convertDurationToString(convertTimestampToDuration(+ageMax, Calendar.DAY)) + } + break + } + case PATIENT_AGE_MONTH: { if (value?.includes('ge')) { const ageMin = value?.replace('ge', '') - currentCriterion.age[0] = convertDurationToString(convertTimestampToDuration(+ageMin)) + currentCriterion.age[0] = convertDurationToString(convertTimestampToDuration(+ageMin, Calendar.MONTH)) } else if (value?.includes('le')) { const ageMax = value?.replace('le', '') - currentCriterion.age[1] = convertDurationToString(convertTimestampToDuration(+ageMax)) + currentCriterion.age[1] = convertDurationToString(convertTimestampToDuration(+ageMax, Calendar.MONTH)) } break } @@ -952,21 +976,25 @@ export async function unbuildRequest(_json: string): Promise { case ENCOUNTER_DURATION: { if (value.includes('ge')) { const durationMin = value?.replace('ge', '') - currentCriterion.duration[0] = convertDurationToString(convertTimestampToDuration(+durationMin)) + currentCriterion.duration[0] = convertDurationToString( + convertTimestampToDuration(+durationMin, Calendar.DAY) + ) } else if (value.includes('le')) { const durationMax = value?.replace('le', '') - currentCriterion.duration[1] = convertDurationToString(convertTimestampToDuration(+durationMax)) + currentCriterion.duration[1] = convertDurationToString( + convertTimestampToDuration(+durationMax, Calendar.DAY) + ) } break } case ENCOUNTER_MIN_BIRTHDATE: { const ageMin = value?.replace('ge', '') - currentCriterion.age[0] = convertDurationToString(convertTimestampToDuration(+ageMin)) + currentCriterion.age[0] = convertDurationToString(convertTimestampToDuration(+ageMin, Calendar.DAY)) break } case ENCOUNTER_MAX_BIRTHDATE: { const ageMax = value?.replace('le', '') - currentCriterion.age[1] = convertDurationToString(convertTimestampToDuration(+ageMax)) + currentCriterion.age[1] = convertDurationToString(convertTimestampToDuration(+ageMax, Calendar.DAY)) break } case ENCOUNTER_ENTRYMODE: {